Mitigating CVE-2025-67288 in Umbraco 13 (if you feel you need 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
| Claim | Reality |
|---|---|
| Upload a crafted PDF → JavaScript runs | The JavaScript runs only in the client’s browser (PDF viewer sandbox). |
| Umbraco processes the PDF and executes code | Umbraco does not parse or execute the PDF content. |
| Remote code execution → CVSS 10.0 | No server‑side code execution occurs, so the CVSS score is inflated. |
| XSS because JavaScript runs | The script runs outside the Umbraco domain (sandboxed), so it is not an XSS vulnerability. |
| Browser APIs (fetch, document.cookie) are available | The 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 problem | Exactly – 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
ShouldHandle– UsesFileSignaturesto verify the file really is a PDF (checks the%PDF-header).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
IFileStreamSecurityAnalyzerinterface 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‑434 – Unrestricted 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:
TheFileFormatInspectoris 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
- Implement the code above in your project.
- Upload a PDF that contains JavaScript (e.g., the benign example linked in the original discussion).
- 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.