在 Umbraco 13 中缓解 CVE-2025-67288(如果您觉得有必要)

发布: (2026年2月4日 GMT+8 10:49)
9 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的完整文本内容(除代码块和 URL 之外),我将为您翻译成简体中文并保持原有的格式和 Markdown 语法。

概述

这篇文章是对最近发布的 CVE 的回应,该 CVE 声称 Umbraco 存在严重漏洞。下面是对该说法大体不准确的详细拆解,以及您可以在 Umbraco 项目中实施的实际缓解策略。


TL;DR

  • 该 CVE 基于一个 PDF‑based proof‑of‑concept (PoC),其在 浏览器内部 执行 JavaScript,而非在服务器上。
  • Umbraco 从未以任何方式处理 PDF,从而导致站点本身出现 remote code execution (RCE)cross‑site scripting (XSS)
  • 该问题实际上是 browser‑level 的关注点(PDF 查看器的沙箱),而非 Umbraco 的缺陷。
  • 您仍然可以添加 file‑stream security analyzer,以防止恶意上传(例如,嵌入 JavaScript 的 PDF、SVG 等)。

为什么 CVE 不适用于 Umbraco

声称实际情况
上传精心制作的 PDF → JavaScript 运行JavaScript 仅在 客户端浏览器(PDF 查看器沙箱)中运行。
Umbraco 处理 PDF 并执行代码Umbraco 解析或执行 PDF 内容。
远程代码执行 → CVSS 10.0没有服务器端代码执行,因此 CVSS 分数 被夸大
因为 JavaScript 运行而产生 XSS脚本在 Umbraco 域之外(沙箱)运行,所以 不是 XSS 漏洞。
浏览器 API(fetch、document.cookie)可用PDF 查看器 限制 对危险 API 的访问;仅暴露 PDF 专用 API(例如 app.alert())。
如果恶意 JavaScript 能运行,那就是浏览器的问题正是如此——漏洞在 浏览器,而不是 Umbraco。

注意: Chromium 甚至有一个 FAQ,标题为 “在 PDF 文件中执行 JavaScript 是否意味着存在 XSS 漏洞?”——答案是

实际风险

  • 社会工程 / 钓鱼:精心制作的 PDF 可能诱骗用户点击恶意链接,但这是一种 通用 PDF 查看器问题,并非 Umbraco 缺陷。

在 Umbraco 中缓解恶意文件上传

Umbraco 提供了一个简单的可扩展点:实现 IFileStreamSecurityAnalyzer。平台会对每个上传的文件流调用你的分析器,从而让你能够拒绝不安全的内容。

接口定义

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

官方文档中包含了一个用于防御恶意 SVG 文件的示例。
以下是一个 针对 PDF 的实现,它会扫描嵌入的 JavaScript。

必需的包

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

PDF 安全分析器(兼容 Umbraco)

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 => fals

Source:

e,
            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);
    }
}

工作原理

  1. ShouldHandle – 使用 FileSignatures 验证文件确实是 PDF(检查 %PDF- 头部)。
  2. IsConsideredSafe
    • 使用 PdfPig 打开 PDF(宽容的解析以避免误报)。
    • 遍历整个 PDF 对象图,查找 _suspiciousKeys 中列出的 可疑字典键
    • 只要发现任意此类键,方法即返回 false(拒绝上传)。
    • 任何解析异常也会导致拒绝,以安全为先。

最终思考

  • 此CVE并不代表真实的Umbraco漏洞;这是一种对浏览器如何对PDF JavaScript进行沙箱隔离的误解。
  • 尽管如此,防御恶意上传是最佳实践。IFileStreamSecurityAnalyzer 接口为您提供了一种干净、可测试的方式来强制执行该策略。
  • 上面的示例代码可以直接放入任何 Umbraco 项目,通过 DI 注册后,会自动审查上传的 PDF 是否包含嵌入的 JavaScript 或其他风险操作。

参考文献

  • CWE‑434不受限制的危险类型文件上传
  • Chromium FAQ – “在 PDF 文件中执行 JavaScript 是否意味着存在 XSS 漏洞?”
  • PDF Viewer Design Doc (Chromium) – 有关沙箱隔离和允许的 JavaScript API 的详细信息。

PDF 安全分析器 – 整理后的 Markdown

下面是代码及其说明的整洁版本。
保留了原始结构和内容,只对格式和少量措辞进行了改进。


分析器实现

// 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;
}

注册分析器

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

注意:
FileFormatInspector 被配置为仅查找 PDF 文件,这可以提升性能。
你可以添加其他文件类型,或让它扫描库支持的所有格式——详情请参阅 GitHub 仓库。


如何测试

  1. 在项目中实现上述代码。
  2. 上传包含 JavaScript 的 PDF(例如原讨论中链接的良性示例)。
  3. 分析器将拒绝该文件,并返回类似原教程中展示的错误信息。

警告:
可能会出现一些误报;本示例的目的是演示如何检测并处理这些情况。

Back to Blog

相关文章

阅读更多 »

当 AI 给你一巴掌

当 AI 给你当头一棒:在 Adama 中调试 Claude 生成的代码。你是否曾让 AI “vibe‑code” 一个复杂功能,却花了数小时调试细微的 bug……