Mitigating CVE-2025-67288 in Umbraco 13 (if you feel you need to)

Published: (February 3, 2026 at 09:49 PM EST)
7 min read
Source: Dev.to

Source: Dev.to

Overview

This post is a response to a recently published CVE that claims a serious vulnerability in Umbraco. Below is a detailed breakdown of why the claim is largely inaccurate, followed by a practical mitigation strategy you can implement in your Umbraco projects.


TL;DR

  • The CVE is based on a PDF‑based proof‑of‑concept (PoC) that executes JavaScript inside the browser, not on the server.
  • Umbraco never processes the PDF in a way that would allow remote code execution (RCE) or cross‑site scripting (XSS) on the site itself.
  • The issue is really a browser‑level concern (PDF viewer sandboxing), not an Umbraco flaw.
  • You can still add a file‑stream security analyzer to guard against malicious uploads (e.g., PDFs with embedded JavaScript, SVGs, etc.).

Why the CVE Doesn’t Apply to Umbraco

ClaimReality
Upload a crafted PDF → JavaScript runsThe JavaScript runs only in the client’s browser (PDF viewer sandbox).
Umbraco processes the PDF and executes codeUmbraco does not parse or execute the PDF content.
Remote code execution → CVSS 10.0No server‑side code execution occurs, so the CVSS score is inflated.
XSS because JavaScript runsThe script runs outside the Umbraco domain (sandboxed), so it is not an XSS vulnerability.
Browser APIs (fetch, document.cookie) are availableThe PDF viewer restricts access to dangerous APIs; only PDF‑specific APIs (e.g., app.alert()) are exposed.
If malicious JavaScript could run, it would be the browser’s problemExactly – the vulnerability would be in the browser, not in Umbraco.

Note: Chromium even has an FAQ titled “Does executing JavaScript in a PDF file mean there’s an XSS vulnerability?” – the answer is no.

The Real Risk

  • Social engineering / phishing: a crafted PDF could trick users into clicking malicious links, but that is a general PDF‑viewer issue, not an Umbraco flaw.

Mitigating Malicious File Uploads in Umbraco

Umbraco provides a simple extensibility point: implement IFileStreamSecurityAnalyzer. The platform will invoke your analyzer for every uploaded file stream, allowing you to reject unsafe content.

Interface Definition

public interface IFileStreamSecurityAnalyzer
{
    /// <summary>
    /// Indicates whether the analyzer should process the file.
    /// The implementation should be considerably faster than IsConsideredSafe.
    /// </summary>
    /// <param name="fileStream">The uploaded file stream.</param>
    /// <returns>True if the analyzer wants to handle this file.</returns>
    bool ShouldHandle(Stream fileStream);

    /// <summary>
    /// Analyzes whether the file content is considered safe.
    /// </summary>
    /// <param name="fileStream">Needs to be a read/write seekable stream.</param>
    /// <returns>True if the file is considered safe.</returns>
    bool IsConsideredSafe(Stream fileStream);
}

The official docs include an example that protects against malicious SVG files.
Below is a PDF‑specific implementation that scans for embedded JavaScript.

Required Packages

dotnet add package FileSignatures   # For quick file‑type detection via header signatures
dotnet add package PdfPig          # Lightweight PDF parser (MIT‑licensed)

PDF Security Analyzer (Umbraco‑compatible)

using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Logging;
using PdfPig;
using PdfPig.Core;
using PdfPig.Parser;
using PdfPig.Tokens;
using Umbraco.Cms.Core.IO;

internal class PdfSecurityAnalyzer(
    ILogger logger,
    IFileFormatInspector fileFormatInspector) : IFileStreamSecurityAnalyzer
{
    // PDF dictionary keys that indicate potentially dangerous actions
    private static readonly HashSet<string> _suspiciousKeys = new()
    {
        "AA",           // Additional Actions
        "OpenAction",   // Actions triggered on document open
        "JS",           // JavaScript stream
        "JavaScript",   // Explicit JavaScript name
        "Launch",       // Launch external applications
        "SubmitForm",   // Form submission (possible data exfiltration)
        "ImportData"    // Import external data
    };

    // -----------------------------------------------------------------
    // IFileStreamSecurityAnalyzer implementation
    // -----------------------------------------------------------------
    public bool ShouldHandle(Stream fileStream)
    {
        // Quick header check – PDF files start with "%PDF-"
        if (!fileFormatInspector.IsPdf(fileStream))
            return false;

        // Reset stream position for later processing
        fileStream.Position = 0;
        return true;
    }

    public bool IsConsideredSafe(Stream fileStream)
    {
        fileStream.Position = 0;

        var options = new ParsingOptions { UseLenientParsing = true };

        try
        {
            using var document = PdfDocument.Open(fileStream, options);
            var visited = new HashSet<IndirectReference>();

            // Start scanning from the catalog (root of the PDF structure)
            if (ContainsSuspiciousContent(document.Structure.Catalog.CatalogDictionary, document, visited))
                return false;   // Unsafe content found

            return true;        // No suspicious keys detected
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to parse PDF for security analysis");
            return false;       // Parsing failure → treat as unsafe
        }
    }

    // -----------------------------------------------------------------
    // Recursive PDF token inspection
    // -----------------------------------------------------------------
    private bool ContainsSuspiciousContent(IToken token, PdfDocument document, HashSet<IndirectReference> visited)
    {
        return token switch
        {
            null => false,
            DictionaryToken dict => CheckDictionary(dict, document, visited),
            ArrayToken array => CheckArray(array, document, visited),
            IndirectReferenceToken refToken => CheckIndirectReference(refToken, document, visited),
            _ => false
        };
    }

    private bool CheckDictionary(DictionaryToken dict, PdfDocument document, HashSet<IndirectReference> visited)
    {
        foreach (var kvp in dict.Data)
        {
            // If the key itself is suspicious, reject immediately
            if (_suspiciousKeys.Contains(kvp.Key.Value))
                return true;

            // Recursively inspect the value token
            if (ContainsSuspiciousContent(kvp.Value, document, visited))
                return true;
        }
        return false;
    }

    private bool CheckArray(ArrayToken array, PdfDocument document, HashSet<IndirectReference> visited)
    {
        foreach (var item in array.Data)
        {
            if (ContainsSuspiciousContent(item, document, visited))
                return true;
        }
        return false;
    }

    private bool CheckIndirectReference(IndirectReferenceToken refToken, PdfDocument document, HashSet<IndirectReference> visited)
    {
        var reference = new IndirectReference(refToken.ObjectNumber, refToken.GenerationNumber);
        if (visited.Contains(reference))
            return false; // Prevent infinite loops on circular references

        visited.Add(reference);
        var resolved = document.GetObject(reference);
        return ContainsSuspiciousContent(resolved, document, visited);
    }
}

How It Works

  1. ShouldHandle – Uses FileSignatures to verify the file really is a PDF (checks the %PDF- header).
  2. IsConsideredSafe
    • Opens the PDF with PdfPig (lenient parsing to avoid false negatives).
    • Traverses the entire PDF object graph, looking for any of the suspicious dictionary keys listed in _suspiciousKeys.
    • If any such key is found, the method returns false (reject the upload).
    • Any parsing exception also results in a rejection, favoring safety.

Final Thoughts

  • The CVE in question does not represent a real Umbraco vulnerability; it is a misunderstanding of how browsers sandbox PDF JavaScript.
  • Nevertheless, defending against malicious uploads is a best practice. The IFileStreamSecurityAnalyzer interface gives you a clean, testable way to enforce that policy.
  • The sample code above can be dropped into any Umbraco project, registered via DI, and will automatically vet uploaded PDFs for embedded JavaScript or other risky actions.

References

  • CWE‑434Unrestricted Upload of File with Dangerous Type
  • Chromium FAQ – “Does executing JavaScript in a PDF file mean there’s an XSS vulnerability?”
  • PDF Viewer Design Doc (Chromium) – Details on sandboxing and allowed JavaScript APIs.

PDF Security Analyzer – Cleaned‑up Markdown

Below is a tidy version of the code and accompanying explanation.
The structure and original content are preserved; only formatting and minor wording have been improved.


Analyzer Implementation

// Main entry point – decides whether a token contains suspicious content
private bool ContainsSuspiciousContent(
    PdfToken token,
    PdfDocument document,
    HashSet<IndirectReference> visited)
{
    return token switch
    {
        StreamToken stream => ContainsSuspiciousContent(stream.StreamDictionary, document, visited),
        ObjectToken objToken => ContainsSuspiciousContent(objToken.Data, document, visited),
        _ => false
    };
}

// ---------------------------------------------------------------------
// Dictionary handling
private bool CheckDictionary(
    DictionaryToken dict,
    PdfDocument document,
    HashSet<IndirectReference> visited)
{
    // Look for suspicious keys
    foreach (var keyName in dict.Data.Keys)
    {
        if (_suspiciousKeys.Contains(keyName))
            return true;

        // Subtype check: /S /JavaScript
        if (keyName == "S" &&
            dict.TryGet(NameToken.S, out NameToken subtype) &&
            subtype.Data == "JavaScript")
        {
            return true;
        }
    }

    // Recurse into all dictionary values
    return dict.Data.Values.Any(value => ContainsSuspiciousContent(value, document, visited));
}

// ---------------------------------------------------------------------
// Array handling
private bool CheckArray(
    ArrayToken array,
    PdfDocument document,
    HashSet<IndirectReference> visited)
{
    return array.Data.Any(item => ContainsSuspiciousContent(item, document, visited));
}

// ---------------------------------------------------------------------
// Indirect reference handling
private bool CheckIndirectReference(
    IndirectReferenceToken refToken,
    PdfDocument document,
    HashSet<IndirectReference> visited)
{
    if (visited.Contains(refToken.Data))
        return false;

    visited.Add(refToken.Data);

    try
    {
        var actualToken = document.Structure.GetObject(refToken.Data);
        return ContainsSuspiciousContent(actualToken, document, visited);
    }
    catch
    {
        // If the object cannot be resolved we treat it as non‑suspicious
        return false;
    }
}

// ---------------------------------------------------------------------
// File‑type detection
public bool ShouldHandle(Stream fileStream)
{
    var format = fileFormatInspector.DetermineFileFormat(fileStream);

    return format is Pdf;
}

Registering the Analyzer

public class RegisterFileStreamSecurityAnalysers : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        // Limit the inspector to PDF files for better performance
        var recognised = FileFormatLocator.GetFormats().OfType<Pdf>();
        var inspector = new FileFormatInspector(recognised);
        builder.Services.AddSingleton(inspector);

        // Register the PDF security analyzer
        builder.Services.AddSingleton<IFileStreamSecurityAnalyzer, PdfSecurityAnalyzer>();
    }
}

Note:
The FileFormatInspector is configured to look only for PDFs, which improves performance.
You can add additional file types or let it scan all formats supported by the library – see the GitHub repository for details.


How to Test

  1. Implement the code above in your project.
  2. Upload a PDF that contains JavaScript (e.g., the benign example linked in the original discussion).
  3. The analyzer will reject the file and return an error message similar to the one shown in the original tutorial.

Caveat:
Some false positives are possible; the purpose of this example is to illustrate how to detect and handle them.

Back to Blog

Related posts

Read more »