VYPR
High severity8.8NVD Advisory· Published May 14, 2024· Updated May 12, 2026

CVE-2024-4367

CVE-2024-4367

Description

A type check was missing when handling fonts in PDF.js, which would allow arbitrary JavaScript execution in the PDF.js context. This vulnerability affects Firefox < 126, Firefox ESR < 115.11, and Thunderbird < 115.11.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2024-4367 is a missing type check in PDF.js font handling that allows arbitrary JavaScript execution in the PDF.js context, affecting Firefox, Thunderbird, and numerous web applications.

Vulnerability

Analysis

CVE-2024-4367 involves a missing type check when PDF.js processes fonts, specifically Type 1 fonts, in the FontFaceObject.getPathGenerator() method. The glyph path compilation process constructs JavaScript statements from command arguments without verifying their types, which allows an attacker to inject arbitrary JavaScript code when isEvalSupported is enabled (the default setting) [1][2]. This occurs in the src/display/font_loader.js module, where command arguments are joined directly into a JavaScript function string [1].

Exploitation

Exploitation requires a user to open a malicious PDF file, after which arbitrary JavaScript executes in the PDF.js context, not the general JavaScript sandbox. This is a form of universal cross-site scripting (XSS) that works within Firefox's built-in PDF viewer and any web or Electron application using a vulnerable version of PDF.js [2]. An attacker can craft a PDF with a malicious Type 1 font that triggers the injection during font rendering [1].

Impact

Once executed, the attacker gains control over the PDF viewer window. They can spy on user activity, trigger downloads via "pdf.js.message" events (including from file:// URLs), and extract the PDF's local file path from window.PDFViewerApplication.url [1][2]. When PDF.js is integrated into a web application, this enables stored XSS on the application's origin [2].

Mitigation

Mozilla has fixed this vulnerability in Firefox 126, Firefox ESR 115.11, and Thunderbird 115.11 [1][4]. Applications using the pdfjs-dist npm package should update to a patched version (check advisories). Affected users should apply these updates immediately [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pdfjs-distnpm
< 4.2.674.2.67

Affected products

3

Patches

1
85e64b5c16c9

Merge pull request #18015 from calixteman/rm_eval_font_loader

https://github.com/mozilla/pdf.jscalixtemanApr 28, 2024via ghsa
5 files changed · +150 61
  • src/core/evaluator.js+10 1 modified
    @@ -4391,6 +4391,15 @@ class PartialEvaluator {
           }
         }
     
    +    let fontMatrix = dict.getArray("FontMatrix");
    +    if (
    +      !Array.isArray(fontMatrix) ||
    +      fontMatrix.length !== 6 ||
    +      fontMatrix.some(x => typeof x !== "number")
    +    ) {
    +      fontMatrix = FONT_IDENTITY_MATRIX;
    +    }
    +
         const properties = {
           type,
           name: fontName.name,
    @@ -4403,7 +4412,7 @@ class PartialEvaluator {
           loadedName: baseDict.loadedName,
           composite,
           fixedPitch: false,
    -      fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
    +      fontMatrix,
           firstChar,
           lastChar,
           toUnicode,
    
  • src/core/font_renderer.js+51 26 modified
    @@ -16,6 +16,7 @@
     import {
       bytesToString,
       FONT_IDENTITY_MATRIX,
    +  FontRenderOps,
       FormatError,
       unreachable,
       warn,
    @@ -180,13 +181,13 @@ function lookupCmap(ranges, unicode) {
     
     function compileGlyf(code, cmds, font) {
       function moveTo(x, y) {
    -    cmds.push({ cmd: "moveTo", args: [x, y] });
    +    cmds.add(FontRenderOps.MOVE_TO, [x, y]);
       }
       function lineTo(x, y) {
    -    cmds.push({ cmd: "lineTo", args: [x, y] });
    +    cmds.add(FontRenderOps.LINE_TO, [x, y]);
       }
       function quadraticCurveTo(xa, ya, x, y) {
    -    cmds.push({ cmd: "quadraticCurveTo", args: [xa, ya, x, y] });
    +    cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]);
       }
     
       let i = 0;
    @@ -247,20 +248,22 @@ function compileGlyf(code, cmds, font) {
           if (subglyph) {
             // TODO: the transform should be applied only if there is a scale:
             // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205
    -        cmds.push(
    -          { cmd: "save" },
    -          {
    -            cmd: "transform",
    -            args: [scaleX, scale01, scale10, scaleY, x, y],
    -          }
    -        );
    +        cmds.add(FontRenderOps.SAVE);
    +        cmds.add(FontRenderOps.TRANSFORM, [
    +          scaleX,
    +          scale01,
    +          scale10,
    +          scaleY,
    +          x,
    +          y,
    +        ]);
     
             if (!(flags & 0x02)) {
               // TODO: we must use arg1 and arg2 to make something similar to:
               // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1209
             }
             compileGlyf(subglyph, cmds, font);
    -        cmds.push({ cmd: "restore" });
    +        cmds.add(FontRenderOps.RESTORE);
           }
         } while (flags & 0x20);
       } else {
    @@ -365,13 +368,13 @@ function compileGlyf(code, cmds, font) {
     
     function compileCharString(charStringCode, cmds, font, glyphId) {
       function moveTo(x, y) {
    -    cmds.push({ cmd: "moveTo", args: [x, y] });
    +    cmds.add(FontRenderOps.MOVE_TO, [x, y]);
       }
       function lineTo(x, y) {
    -    cmds.push({ cmd: "lineTo", args: [x, y] });
    +    cmds.add(FontRenderOps.LINE_TO, [x, y]);
       }
       function bezierCurveTo(x1, y1, x2, y2, x, y) {
    -    cmds.push({ cmd: "bezierCurveTo", args: [x1, y1, x2, y2, x, y] });
    +    cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]);
       }
     
       const stack = [];
    @@ -544,7 +547,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
                 const bchar = stack.pop();
                 y = stack.pop();
                 x = stack.pop();
    -            cmds.push({ cmd: "save" }, { cmd: "translate", args: [x, y] });
    +            cmds.add(FontRenderOps.SAVE);
    +            cmds.add(FontRenderOps.TRANSLATE, [x, y]);
                 let cmap = lookupCmap(
                   font.cmap,
                   String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])
    @@ -555,7 +559,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
                   font,
                   cmap.glyphId
                 );
    -            cmds.push({ cmd: "restore" });
    +            cmds.add(FontRenderOps.RESTORE);
     
                 cmap = lookupCmap(
                   font.cmap,
    @@ -741,6 +745,27 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
     
     const NOOP = [];
     
    +class Commands {
    +  cmds = [];
    +
    +  add(cmd, args) {
    +    if (args) {
    +      if (args.some(arg => typeof arg !== "number")) {
    +        warn(
    +          `Commands.add - "${cmd}" has at least one non-number arg: "${args}".`
    +        );
    +        // "Fix" the wrong args by replacing them with 0.
    +        const newArgs = args.map(arg => (typeof arg === "number" ? arg : 0));
    +        this.cmds.push(cmd, ...newArgs);
    +      } else {
    +        this.cmds.push(cmd, ...args);
    +      }
    +    } else {
    +      this.cmds.push(cmd);
    +    }
    +  }
    +}
    +
     class CompiledFont {
       constructor(fontMatrix) {
         if (this.constructor === CompiledFont) {
    @@ -757,8 +782,10 @@ class CompiledFont {
         let fn = this.compiledGlyphs[glyphId];
         if (!fn) {
           try {
    -        fn = this.compileGlyph(this.glyphs[glyphId], glyphId);
    -        this.compiledGlyphs[glyphId] = fn;
    +        fn = this.compiledGlyphs[glyphId] = this.compileGlyph(
    +          this.glyphs[glyphId],
    +          glyphId
    +        );
           } catch (ex) {
             // Avoid attempting to re-compile a corrupt glyph.
             this.compiledGlyphs[glyphId] = NOOP;
    @@ -793,16 +820,14 @@ class CompiledFont {
           }
         }
     
    -    const cmds = [
    -      { cmd: "save" },
    -      { cmd: "transform", args: fontMatrix.slice() },
    -      { cmd: "scale", args: ["size", "-size"] },
    -    ];
    +    const cmds = new Commands();
    +    cmds.add(FontRenderOps.SAVE);
    +    cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice());
    +    cmds.add(FontRenderOps.SCALE);
         this.compileGlyphImpl(code, cmds, glyphId);
    +    cmds.add(FontRenderOps.RESTORE);
     
    -    cmds.push({ cmd: "restore" });
    -
    -    return cmds;
    +    return cmds.cmds;
       }
     
       compileGlyphImpl() {
    
  • src/display/api.js+2 4 modified
    @@ -169,8 +169,8 @@ const DefaultStandardFontDataFactory =
      *   pixels, i.e. width * height. Images above this value will not be rendered.
      *   Use -1 for no limit, which is also the default value.
      * @property {boolean} [isEvalSupported] - Determines if we can evaluate strings
    - *   as JavaScript. Primarily used to improve performance of font rendering, and
    - *   when parsing PDF functions. The default value is `true`.
    + *   as JavaScript. Primarily used to improve performance of PDF functions.
    + *   The default value is `true`.
      * @property {boolean} [isOffscreenCanvasSupported] - Determines if we can use
      *   `OffscreenCanvas` in the worker. Primarily used to improve performance of
      *   image conversion/rendering.
    @@ -384,7 +384,6 @@ function getDocument(src) {
       };
       const transportParams = {
         ignoreErrors,
    -    isEvalSupported,
         disableFontFace,
         fontExtraProperties,
         enableXfa,
    @@ -2744,7 +2743,6 @@ class WorkerTransport {
                   ? (font, url) => globalThis.FontInspector.fontAdded(font, url)
                   : null;
               const font = new FontFaceObject(exportedData, {
    -            isEvalSupported: params.isEvalSupported,
                 disableFontFace: params.disableFontFace,
                 ignoreErrors: params.ignoreErrors,
                 inspectFont,
    
  • src/display/font_loader.js+74 30 modified
    @@ -16,7 +16,7 @@
     import {
       assert,
       bytesToString,
    -  FeatureTest,
    +  FontRenderOps,
       isNodeJS,
       shadow,
       string32,
    @@ -362,19 +362,13 @@ class FontLoader {
     class FontFaceObject {
       constructor(
         translatedData,
    -    {
    -      isEvalSupported = true,
    -      disableFontFace = false,
    -      ignoreErrors = false,
    -      inspectFont = null,
    -    }
    +    { disableFontFace = false, ignoreErrors = false, inspectFont = null }
       ) {
         this.compiledGlyphs = Object.create(null);
         // importing translated data
         for (const i in translatedData) {
           this[i] = translatedData[i];
         }
    -    this.isEvalSupported = isEvalSupported !== false;
         this.disableFontFace = disableFontFace === true;
         this.ignoreErrors = ignoreErrors === true;
         this._inspectFont = inspectFont;
    @@ -440,35 +434,85 @@ class FontFaceObject {
             throw ex;
           }
           warn(`getPathGenerator - ignoring character: "${ex}".`);
    +    }
     
    +    if (!Array.isArray(cmds) || cmds.length === 0) {
           return (this.compiledGlyphs[character] = function (c, size) {
             // No-op function, to allow rendering to continue.
           });
         }
     
    -    // 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");
    +    const commands = [];
    +    for (let i = 0, ii = cmds.length; i < ii; ) {
    +      switch (cmds[i++]) {
    +        case FontRenderOps.BEZIER_CURVE_TO:
    +          {
    +            const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
    +            commands.push(ctx => ctx.bezierCurveTo(a, b, c, d, e, f));
    +            i += 6;
    +          }
    +          break;
    +        case FontRenderOps.MOVE_TO:
    +          {
    +            const [a, b] = cmds.slice(i, i + 2);
    +            commands.push(ctx => ctx.moveTo(a, b));
    +            i += 2;
    +          }
    +          break;
    +        case FontRenderOps.LINE_TO:
    +          {
    +            const [a, b] = cmds.slice(i, i + 2);
    +            commands.push(ctx => ctx.lineTo(a, b));
    +            i += 2;
    +          }
    +          break;
    +        case FontRenderOps.QUADRATIC_CURVE_TO:
    +          {
    +            const [a, b, c, d] = cmds.slice(i, i + 4);
    +            commands.push(ctx => ctx.quadraticCurveTo(a, b, c, d));
    +            i += 4;
    +          }
    +          break;
    +        case FontRenderOps.RESTORE:
    +          commands.push(ctx => ctx.restore());
    +          break;
    +        case FontRenderOps.SAVE:
    +          commands.push(ctx => ctx.save());
    +          break;
    +        case FontRenderOps.SCALE:
    +          // The scale command must be at the third position, after save and
    +          // transform (for the font matrix) commands (see also
    +          // font_renderer.js).
    +          // The goal is to just scale the canvas and then run the commands loop
    +          // without the need to pass the size parameter to each command.
    +          assert(
    +            commands.length === 2,
    +            "Scale command is only valid at the third position."
    +          );
    +          break;
    +        case FontRenderOps.TRANSFORM:
    +          {
    +            const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
    +            commands.push(ctx => ctx.transform(a, b, c, d, e, f));
    +            i += 6;
    +          }
    +          break;
    +        case FontRenderOps.TRANSLATE:
    +          {
    +            const [a, b] = cmds.slice(i, i + 2);
    +            commands.push(ctx => ctx.translate(a, b));
    +            i += 2;
    +          }
    +          break;
           }
    -      // eslint-disable-next-line no-new-func
    -      return (this.compiledGlyphs[character] = new Function(
    -        "c",
    -        "size",
    -        jsBuf.join("")
    -      ));
    -    }
    -    // ... but fall back on using Function.prototype.apply() if we're
    -    // blocked from using eval() for whatever reason (like CSP policies).
    -    return (this.compiledGlyphs[character] = function (c, size) {
    -      for (const current of cmds) {
    -        if (current.cmd === "scale") {
    -          current.args = [size, -size];
    -        }
    -        // eslint-disable-next-line prefer-spread
    -        c[current.cmd].apply(c, current.args);
    +    }
    +
    +    return (this.compiledGlyphs[character] = function glyphDrawer(ctx, size) {
    +      commands[0](ctx);
    +      commands[1](ctx);
    +      ctx.scale(size, -size);
    +      for (let i = 2, ii = commands.length; i < ii; i++) {
    +        commands[i](ctx);
           }
         });
       }
    
  • src/shared/util.js+13 0 modified
    @@ -1073,6 +1073,18 @@ function getUuid() {
     
     const AnnotationPrefix = "pdfjs_internal_id_";
     
    +const FontRenderOps = {
    +  BEZIER_CURVE_TO: 0,
    +  MOVE_TO: 1,
    +  LINE_TO: 2,
    +  QUADRATIC_CURVE_TO: 3,
    +  RESTORE: 4,
    +  SAVE: 5,
    +  SCALE: 6,
    +  TRANSFORM: 7,
    +  TRANSLATE: 8,
    +};
    +
     export {
       AbortException,
       AnnotationActionEventType,
    @@ -1095,6 +1107,7 @@ export {
       DocumentActionEventType,
       FeatureTest,
       FONT_IDENTITY_MATRIX,
    +  FontRenderOps,
       FormatError,
       getModificationDate,
       getUuid,
    

Vulnerability mechanics

Root cause

"Missing type validation and the use of dynamic JavaScript generation (eval) when processing font glyph instructions and properties allowed for arbitrary code execution."

Attack vector

An attacker can trigger this vulnerability by crafting a malicious PDF file containing a specially formatted font. When the PDF is processed, the lack of type checking and the reliance on dynamic JavaScript generation allowed the attacker to inject arbitrary code into the PDF.js context [patch_id=26922]. This occurs during the font rendering process, specifically when the engine processes glyph drawing instructions or font properties like `FontMatrix`.

Affected code

The vulnerability exists in `src/display/font_loader.js` and `src/core/font_renderer.js`, where font glyph drawing instructions were previously compiled into JavaScript using `new Function()` [patch_id=26922]. The patch replaces this dynamic code generation with a safer, command-based execution system using `FontRenderOps` and a `Commands` class to validate arguments. Additionally, `src/core/evaluator.js` was updated to enforce strict type checking on the `FontMatrix` property.

What the fix does

The patch removes the use of `new Function()` for compiling glyph drawing instructions, which was the primary mechanism for arbitrary JavaScript execution [patch_id=26922]. It introduces a `FontRenderOps` enumeration and a `Commands` class to define and validate allowed operations and their arguments, ensuring only safe, predefined operations are executed. Furthermore, the patch adds explicit type validation for the `FontMatrix` array in `src/core/evaluator.js` to ensure all elements are numbers, preventing potential type confusion attacks.

Preconditions

  • inputThe user must open a maliciously crafted PDF file in an affected version of Firefox or Thunderbird.

Generated on May 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

21

News mentions

4