CVE-2025-67288을 Umbraco 13에서 완화하기 (필요하다고 생각되면)

발행: (2026년 2월 4일 오전 11:49 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.

개요

이 게시물은 최근 발표된 CVE에 대한 응답이며, Umbraco에서 심각한 취약점이 있다고 주장합니다. 아래는 해당 주장이 대부분 부정확한 이유에 대한 자세한 분석과 Umbraco 프로젝트에 적용할 수 있는 실용적인 완화 전략을 제공합니다.


요약

  • CVE는 서버가 아닌 브라우저 내부에서 JavaScript를 실행하는 **PDF‑기반 증명 개념(PoC)**에 기반합니다.
  • Umbraco는 PDF를 처리하여 사이트 자체에서 원격 코드 실행(RCE) 또는 **교차 사이트 스크립팅(XSS)**이 발생하도록 하지 않습니다.
  • 이 문제는 실제로 브라우저‑수준의 우려사항(PDF 뷰어 샌드박싱)이며, Umbraco 결함이 아닙니다.
  • 여전히 악성 업로드(예: 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에 있는 것이 아닙니다.

Note: Chromium은 “PDF 파일에서 JavaScript를 실행한다는 것이 XSS 취약점을 의미하나요?” 라는 FAQ를 가지고 있으며, 답은 아니오입니다.

실제 위험

  • 사회공학 / 피싱: 조작된 PDF가 사용자를 속여 악성 링크를 클릭하게 할 수 있지만, 이는 일반적인 PDF 뷰어 문제이며 Umbraco 결함이 아닙니다.

Source:

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

```csharp
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. ShouldHandleFileSignatures를 사용하여 파일이 실제 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에 대한 세부 정보.

Source:

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

Note:
FileFormatInspectorPDF만 찾도록 구성되어 있어 성능이 향상됩니다.
추가 파일 형식을 넣거나 라이브러리가 지원하는 모든 형식을 스캔하도록 할 수 있습니다 – 자세한 내용은 GitHub 저장소를 참고하세요.


테스트 방법

  1. 위 코드를 프로젝트에 구현합니다.
  2. JavaScript가 포함된 PDF(예: 원래 토론에 링크된 무해한 예시)를 업로드합니다.
  3. 분석기가 파일을 거부하고 원본 튜토리얼에 표시된 것과 유사한 오류 메시지를 반환합니다.

Caveat:
일부 오탐이 발생할 수 있습니다; 이 예제는 그러한 상황을 감지하고 처리하는 방법을 보여주기 위한 것입니다.

Back to Blog

관련 글

더 보기 »

AI가 당신에게 뺨을 때릴 때

AI가 당신을 뺨 때릴 때: Adama에서 Claude가 생성한 코드 디버깅 AI에게 복잡한 기능을 “vibe‑code”하게 맡겨본 적이 있나요? 그 결과 미묘한 버그를 디버깅하느라 몇 시간을 보내게 됩니다.