Closed Bug 1939692 Opened 1 year ago Closed 1 year ago

Arbitrary Javascript injection in PDF.js through FontMatrix

Categories

(Firefox :: PDF Viewer, defect)

Firefox 133
Desktop
Windows 10
defect

Tracking

()

VERIFIED DUPLICATE of bug 1893645

People

(Reporter: 0xrai, Unassigned)

Details

Attachments

(2 files)

Attached image pdf.png

(Tested with Firefox 124.0b9 on Ubuntu and with PDF.js latest from Git.)

Arbitrary Javascript code can be executed in the PDF.js context (unrelated to the Javascript sandbox) by abusing a lack of type checking in the glyph path compilation process, specifically when a Type 1 font is involved. A prerequisite is that isEvalSupported needs to be true (it is by default).

This results in a somewhat universal XSS when a PDF is opened with Firefox (cross-origin restrictions are still in place). An attacker can control the PDF.js window and do things like spy on the user’s activity, trigger downloads using pdf.js.message events (even file:// URLs can be “downloaded” in this way) or leak the PDF’s local file path from window.PDFViewerApplication.url.

In the cases where PDF.js is used in a web-application, this leads to a stored XSS attack on the respective page’s origin.

Details

The method FontFaceObject.getPathGenerator(...) compiles glyphs to Javascript functions that will return the associated path. In case isEvalSupported is set to true, it does this by concatenating them into a list of Javascript statements and creating a Function object out of that: (https://github.com/mozilla/pdf.js/blob/90d4b9c2c0df7cadbd5c2388431b208f08da1ed6/src/display/font_loader.js#L450 )

    // 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
      return (this.compiledGlyphs[character] = new Function(
        "c",
        "size",
        jsBuf.join("")
      ));
    }

This list of commands, cmds, is generated by the method CompiledFont.compileGlyph(...) and the compileGlyphImpl variants for the different font types: (trimmed from https://github.com/mozilla/pdf.js/blob/90d4b9c2c0df7cadbd5c2388431b208f08da1ed6/src/core/font_renderer.js#L798 )

  compileGlyph(code, glyphId) {
    ...
    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;
  }

For the arguments of the transform command it takes the fontMatrix array from the current font object. It turns out that under specific circumstances (seemingly only for Type1 fonts) this array is grabbed from a PDF dictionary and not (also) from the font data itself. Hence, the array can contain arbitrary PDF primitives including strings. The retrieval happens in PartialEvaluator.translateFont:

    const properties = {
      ...
      fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
      ...
    };

(Note: with other font types this does not appear to be a problem as the FontMatrix is later grabbed from the font data itself (e.g. CFF dicts or Type1 “PostScript” parsing) where the parsers explicitly expect numeric array elements. Even if it is not present in the font data, it is overwritten with a standard matrix.)

This means that string-type elements get concatenated literally in the resulting Javascript. Hence we can craft a PDF with a Type1 font and a Font object with this FontMatrix definition;

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

resulting in this Javascript code being executed when the glyph is rendered:

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

Attached is a plain-text minimal PoC.

Possible solution

Type-validation could be added either in compileGlyph, or in translateFont. In the latter case, maybe it is beneficial to implement something like dict.getNumericArray in addition to dict.getArray for situations like this.

Attached file payload1.pdf
OS: Unspecified → Windows 10
Hardware: Unspecified → Desktop
Version: unspecified → Firefox 133
Status: UNCONFIRMED → RESOLVED
Closed: 1 year ago
Duplicate of bug: CVE-2024-4367
Resolution: --- → DUPLICATE
Status: RESOLVED → VERIFIED
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: