DEV Community

Cover image for Mitigating CVE-2025-67288 in Umbraco 13 (if you feel you need to)
Jason Elkin
Jason Elkin

Posted on

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

Firstly, this is a really ill-informed CVE. If you've not read my comment on the Umbraco Forum, I'll repeat the key bits, otherwise feel free to scroll down to the good stuff.

A bit about CVE-2025-67288

If you want the details you can read the full CVE at nvd.nist.gov. Umbraco is in the process of disputing it.

Let me break down why I think it's mostly nonsense...

Remote Code Execution?

It’s an attention grabbing PoC..

  • Upload a crafted PDF file containing embedded JavaScript.
  • Observe that JavaScript from the PDF executes in the browser.

But what does Umbraco have to do with that? You could upload the file anywhere - in fact, and click this link if you dare, I've uploaded that file to a GitHub pages site.
Does that mean GitHub is vulnerable to a CWE-434…? No, and neither is Umbraco.

This is what a CWE-434 means...

The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.

Emphasis mine, but this "CVE" doesn’t pass that test. Umbraco doesn't process the PDF such that arbitrary code can be run on your Umbraco site.

So, remote code execution worthy a CVSS score of 10.0? No.

Is is really XSS?

Look more closely at the XSS (Cross Site Scripting) claim. Where does the JavaScript actually run? In the browser, sure, but it’s not run in the context of the Umbraco site (or GitHub.io, if you were brave and clicked the link above). It’s sandboxed by the browser and not treated like it’s running on your domain at all.

This is such a common misconception that Chromium has an FAQ titled "Does executing JavaScript in a PDF file mean there's an XSS vulnerability?". Spoiler alert, the answer is no.

It's JavaScript, but not as we know it

It doesn’t have access to any important browser APIs - i.e. no fetch() or document.cookies etc. in fact if you look at the alert itself you’ll see it’s calling app.alert()… that’s not a web browser API but a PDF one :face_with_monocle:

If you try and craft a more complex JavaScript payload you’ll quickly find that the browser will block any APIs that could present a real risk to the user.

Even if you could craft malicious JavaScript that executes in a browser, that CVE would really be the browser’s problem, not Umbraco’s.

If you're feeling extra nerdy, you can have a read of the design doc for the PDF Viewer in Chromium. It talks about sandboxing, what kinds of JavaScript execution are allowed, and why.

Obviously there’s still a potential social engineering/phishing angle with these crafted PDFs… but that’s a whole different issue.

But good news, you can still mitigate against it!

Enter the IFileStreamSecurityAnalyzer

It's a simple interface you can implement that Umbraco will then run uploaded filestreams through so you can add any logic you want to check if the files are safe.

public interface IFileStreamSecurityAnalyzer
{
    /// <summary>
    /// Indicates whether the analyzer should process the file
    /// The implementation should be considerably faster than IsConsideredSafe
    /// </summary>
    /// <param name="fileStream"></param>
    /// <returns></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>Whether the file is considered safe</returns>
    bool IsConsideredSafe(Stream fileStream);
}
Enter fullscreen mode Exit fullscreen mode

The docs have a nice example showing you how to protect against a potentially malicious SVG file.

In a similar fashion we can implement our own class that will read a PDF< look for any JavaScript, and return false when IsConsideredSafe() is called.

Notice the ShouldHandle() method only takes the stream. File extensions don't necessarily match up with what's actually in a file so we need to check the contents first to see if this is actually a PDF. A quick way of doing that is to parse just the file signature in the file header.

There's a package for that!

dotnet add package FileSignatures
Enter fullscreen mode Exit fullscreen mode

Then for the PDF reading, I'm using PdfPig:

dotnet add package PdfPig
Enter fullscreen mode Exit fullscreen mode

Here I need to add a disclaimer. The following code is based on something I'm using in production but the PDF library I'm using has rather restrictive/expensive licensing and I wanted to give you a demo that "Just Works" with some friendly OSS libraries - so I asked AI to help me rewrite this for PdfPig.

internal class PdfSecurityAnalyzer(
    ILogger<PdfSecurityAnalyzer> logger,
    IFileFormatInspector fileFormatInspector) : IFileStreamSecurityAnalyzer
{
    private static readonly HashSet<string> _suspiciousKeys =
    [
        "AA",           // Additional Actions
        "OpenAction",   // Actions triggered on document open
        "JS",           // JavaScript Stream
        "JavaScript",   // Explicit JavaScript name
        "Launch",       // Launch external applications
        "SubmitForm",   // Form submission (potential data exfiltration)
        "ImportData"    // Import external data
    ];

    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>();

            // Check the entire document structure starting from catalog
            if (ContainsSuspiciousContent(document.Structure.Catalog.CatalogDictionary, document, visited))
            {
                return false;
            }

            return true;
        }
        catch (Exception ex)
        {
            logger.LogWarning(ex, "Failed to parse PDF for security analysis");
            return false;
        }
    }

    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),
            StreamToken stream => ContainsSuspiciousContent(stream.StreamDictionary, document, visited),
            ObjectToken objToken => ContainsSuspiciousContent(objToken.Data, document, visited),
            _ => false
        };
    }

    private bool CheckDictionary(DictionaryToken dict, PdfDocument document, HashSet<IndirectReference> visited)
    {
        // Check 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));
    }

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

    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
        {
            return false;
        }
    }

    public bool ShouldHandle(Stream fileStream)
    {
        var format = fileFormatInspector.DetermineFileFormat(fileStream);

        if (format is Pdf)
        {
            return true;
        }

        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

And then to register it, use an IComposer:

public class RegisterFileStreamSecurityAnalysers : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        var recognised = FileFormatLocator.GetFormats().OfType<Pdf>();
        var inspector = new FileFormatInspector(recognised);
        builder.Services.AddSingleton<IFileFormatInspector>(inspector);

        builder.Services.AddSingleton<IFileStreamSecurityAnalyzer, PdfSecurityAnalyzer>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice I'm configuring the FileFormatInspector to only look for PDFs, which is better for performance but you can add multiple file types or configure it to look for all types known by the library - have a look at the GitHub Repo for more info.

With the above code implemented, try and upload a PDF containing JavaScript (like the perfectly benign one I linked to above), and you'll get this lovely error message.

You may get some false positives, but this was all about false positives really, wasn't it...

Top comments (0)