Tldr

CVE-2024-4367 reveals a critical vulnerability in Mozilla’s PDF.js library (versions prior to 4.2.67) that allows arbitrary JavaScript execution. The vulnerability stems from insufficient sanitization of embedded JavaScript in PDF files, allowing attackers to bypass security measures through manipulation of the FontMatrix property. This poses significant risks for web applications that use PDF.js to render PDFs from untrusted sources, including Firefox’s built-in PDF viewer and numerous Electron applications.

🌐 Overview

PDF.js, Mozilla’s widely-used JavaScript library for rendering PDF documents in web browsers, suffered from a significant security vulnerability (CVE-2024-4367) discovered by Codean Labs. This vulnerability allows attackers to execute arbitrary JavaScript code when a malicious PDF is opened. The impact is particularly concerning due to two major use cases:

  1. PDF.js serves as Firefox’s built-in PDF viewer, affecting all Firefox users prior to version 126
  2. The library is bundled as pdfjs-dist with approximately 2.7 million weekly downloads on NPM, used in many web and Electron applications for PDF preview functionality

🔍 Technical Details of the Vulnerability

1️⃣ Glyph Rendering

The vulnerability is not related to the PDF format’s scripting functionality but exists in the font rendering code. Fonts in PDFs can come in several formats, and in some cases, PDF.js must manually convert glyphs into curves. For performance, a path generator function is pre-compiled for each glyph using a JavaScript Function object:

// If we can, compile cmds into JS for MAXIMUM SPEED...
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
  const jsBuf = [];
  for (const current of cmds) {
    const args = current.args !== undefined ? current.args.join(",") : "";
    jsBuf.push("c.", current.cmd, "(", args, ");\n");
  }
  // eslint-disable-next-line no-new-func
  console.log(jsBuf.join(""));
  return (this.compiledGlyphs[character] = new Function(
    "c",
    "size",
    jsBuf.join("")
  ));
}

The compileGlyph method initializes the commands array with basic operations:

compileGlyph(code, glyphId) {
  if (!code || code.length === 0 || code[0] === 14) {
    return NOOP;
  }
 
  let fontMatrix = this.fontMatrix;
  ...
 
  const cmds = [
    { cmd: "save" },
    { cmd: "transform", args: fontMatrix.slice() },
    { cmd: "scale", args: ["size", "-size"] },
  ];
  this.compileGlyphImpl(code, cmds, glyphId);
 
  cmds.push({ cmd: "restore" });
 
  return cmds;
}

The generated function code typically looks like:

c.save();
c.transform(0.001, 0, 0, 0.001, 0, 0);
c.scale(size, -size);
c.moveTo(0, 0);
c.restore();

2️⃣ The Vulnerability

The key vulnerability lies in the transform command that uses the fontMatrix array:

{ cmd: "transform", args: fontMatrix.slice() },

This array is inserted directly into the function body, joined by commas. The code assumes it contains only numbers, but if we could inject strings, they would be inserted without quotes, potentially allowing code execution.

3️⃣ FontMatrix Control

While the fontMatrix defaults to [0.001, 0, 0, 0.001, 0, 0], it can be set in the PDF’s metadata. The PartialEvaluator.translateFont method loads attributes from PDF dictionaries:

const properties = {
  type,
  name: fontName.name,
  subtype,
  file: fontFile,
  ...
  fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
  ...
  bbox: descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
  ascent: descriptor.get("Ascent"),
  descent: descriptor.get("Descent"),
  xHeight: descriptor.get("XHeight") || 0,
  capHeight: descriptor.get("CapHeight") || 0,
  flags: descriptor.get("Flags"),
  italicAngle: descriptor.get("ItalicAngle") || 0,
  ...
};

In PDF format, font definitions consist of several objects:

1 0 obj

  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
>>
endobj

2 0 obj

  /Type /FontDescriptor
  /FontName /FooBarFont
  /FontFile 3 0 R
  /ItalicAngle 0
  /Flags 4
>>
endobj

3 0 obj

  /Length 100
>>
... (actual binary font data) ...
endobj

We can define a custom FontMatrix in the Font object:

1 0 obj

  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 6]   % <-----
>>
endobj

PDF supports not just numeric values but also strings (delimited by parentheses):

/FontMatrix [1 2 3 4 5 (foobar)]

When this is processed, the string is inserted directly into the JavaScript:

c.save();
c.transform(1, 2, 3, 4, 5, foobar);
c.scale(size, -size);
c.moveTo(0, 0);
c.restore();

🔧 Exploitation Mechanics

1️⃣ Building a Proof of Concept

Creating a malicious PDF requires defining a custom FontMatrix with string values that contain JavaScript code. For example:

/FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]

The above payload would close the transform() function call prematurely and execute alert('foobar').

2️⃣ Impact Assessment

This vulnerability has varying levels of impact:

  • In Firefox: The JavaScript executes in a sandboxed context under the origin resource://pdf.js, which prevents access to local files but allows certain privileged operations like file downloads. It also allows access to window.PDFViewerApplication.url, leaking the local path of the PDF.

  • In Web Applications: If an application embeds PDF.js without proper sandboxing, the vulnerability essentially provides a cross-site scripting (XSS) primitive on the application’s domain.

  • In Electron Applications: Some Electron apps do not properly sandbox JavaScript code, potentially allowing arbitrary native code execution. Codean Labs confirmed this was the case for at least one popular Electron application.

🛡️ Mitigation and Remediation

1️⃣ Update to Fixed Versions

The most effective mitigation is updating to patched versions:

  • PDF.js version 4.2.67 or higher
  • Firefox 126, Firefox ESR 115.11, and Thunderbird 115.11 or higher
  • Updated wrapper libraries that use PDF.js (e.g., react-pdf)

2️⃣ Alternative Mitigations

If immediate updating is not possible:

  • Set the PDF.js setting isEvalSupported to false to disable the vulnerable code path
  • Implement a strict Content Security Policy (CSP) that disables eval and the Function constructor
  • For applications handling untrusted PDFs, implement proper sandboxing

💻 Affected Versions

Analysis by Rob Wu shows that the vulnerability has been present since the first release of PDF.js, with some exceptions:

  • v4.2.67 (April 29, 2024): unaffected (fixed)
  • v4.1.392 (April 11, 2024): affected
  • v1.10.88 (October 27, 2017): affected
  • v1.9.426 (August 15, 2017): unaffected (due to a typo)
  • v1.5.188 (April 21, 2016): unaffected (due to an accidental typo)
  • v1.4.20 (January 27, 2016): affected
  • v0.8.1181 (April 10, 2014): affected (first public release)

Note that older “unaffected” versions (from 2017 and before) are still vulnerable to a different vulnerability (CVE-2018-5158).

📆 Timeline

  • 2024-04-26: Vulnerability disclosed to Mozilla
  • 2024-04-29: PDF.js v4.2.67 released to NPM, fixing the issue
  • 2024-05-14: Firefox 126, Firefox ESR 115.11, and Thunderbird 115.11 released with fixed version of PDF.js
  • 2024-05-20: Publication of Codean Labs’ blog post
  • 2024-05-22: Detailed version information and updated PoC added by Rob Wu

💁🏼‍♀️ Summary

CVE-2024-4367 demonstrates the security challenges inherent in handling untrusted content, particularly complex formats like PDF. It highlights several important security principles:

  1. Defense in Depth: Relying on a single security check is insufficient; multiple layers of validation are necessary.
  2. Attack Surface Analysis: Complex document formats introduce numerous attack vectors that must be carefully analyzed.
  3. Secure by Default: Security-critical features should follow the principle of least privilege.
  4. Prompt Patching: Mozilla’s quick response helped limit the vulnerability’s impact.

For developers working with PDF processing or any document format supporting embedded code, this vulnerability emphasizes the importance of thorough security reviews, input validation, and proper sandboxing when handling untrusted content.