Skip to content

fix: mitigate CVE-2024-4367 FontMatrix code injection in pdf viewer#351

Merged
DeepDiver1975 merged 5 commits into
masterfrom
fix/OC10-111-cve-2024-4367
Jul 14, 2026
Merged

fix: mitigate CVE-2024-4367 FontMatrix code injection in pdf viewer#351
DeepDiver1975 merged 5 commits into
masterfrom
fix/OC10-111-cve-2024-4367

Conversation

@DeepDiver1975

Copy link
Copy Markdown
Member

Summary

Mitigates CVE-2024-4367 (FontMatrix code injection) in the bundled pdf.js. A missing type check on the PDF /FontMatrix attribute allowed an attacker-controlled string to flow into new Function() in the glyph renderer, executing JavaScript in the viewer origin (XSS → account takeover through a public share / attachment popup).

The pre-existing isEvalSupported=false hardening was bypassable: in workersrc.js it was set after a locale lookup (parent.OC.getLocale()) inside a single try with a bare catch. When the viewer runs top-level (the ?file=blob popup) parent.OC can throw, the catch swallows it, and eval stays enabled. The viewer CSP also explicitly allowed unsafe-eval.

Changes (defense in depth)

  • pdf.worker.js — backport the upstream type check: a /FontMatrix that is not exactly six numbers is replaced by the identity matrix.
  • pdf.js — coerce every glyph-command argument to a number before it is interpolated into new Function() (the intentional scale identifiers are preserved).
  • workersrc.js — set isEvalSupported=false and enableScripting first and unconditionally; isolate the throwing locale lookup; detect the blob popup by parsing the file query parameter for a blob: URL instead of a loose indexOf('?file=blob') substring match (which was satisfiable from the attacker-controlled URL hash).
  • DisplayController — drop allowEvalScript(true) so the new Function sink is unreachable in the browser.

Tests

Adds tests/security/ with the PoC PDF (benign marker payload), node verification harnesses for both source guards, and a blob-popup detection test. All pass locally.

Follow-up

This backports the CVE fix into the current pdf.js 3.15.2 bundle. Upgrading the bundle to pdf.js ≥ 4.2.67 remains a recommended larger follow-up.

Fixes OC10-111

🤖 Generated with Claude Code

@DeepDiver1975 DeepDiver1975 requested a review from a team as a code owner July 14, 2026 11:20
The bundled pdf.js (3.15.2) is affected by CVE-2024-4367: a missing type
check on the PDF /FontMatrix attribute lets an attacker-controlled string
flow into new Function() in the glyph renderer, executing arbitrary
JavaScript in the viewer origin (XSS -> account takeover via the public
share / attachment popup).

The existing isEvalSupported=false hardening was bypassable: in
workersrc.js it ran after a locale lookup (parent.OC.getLocale()) inside a
single try/bare-catch. When the viewer runs top-level (the ?file=blob
popup) parent.OC can throw, the catch swallows it, and eval stays enabled.
The viewer CSP also explicitly allowed unsafe-eval.

Fix in depth:

- pdf.worker.js: backport the upstream type check - a /FontMatrix that is
  not exactly six numbers is replaced by the identity matrix.
- pdf.js: coerce every glyph-command argument to a number before it is
  interpolated into new Function(), so no string can be injected even if a
  bad matrix slips through (the intentional "scale" identifiers are kept).
- workersrc.js: set isEvalSupported=false and enableScripting first and
  unconditionally, isolate the throwing locale lookup, and detect the blob
  popup by parsing the "file" query parameter for a blob: URL instead of a
  loose indexOf('?file=blob') substring match (which was satisfiable from
  the attacker-controlled URL hash).
- DisplayController: explicitly disable eval in the CSP with
  allowEvalScript(false). Core's ContentSecurityPolicy allows 'unsafe-eval' by
  default, so merely dropping allowEvalScript(true) is not enough - the new
  Function sink stays reachable unless eval is explicitly disabled.

Adds regression fixtures and node verification harnesses under
tests/security/.

Fixes OC10-111

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
@DeepDiver1975 DeepDiver1975 force-pushed the fix/OC10-111-cve-2024-4367 branch from f2a2219 to 028c71e Compare July 14, 2026 11:36
showPdfViewer() now disables 'unsafe-eval' (allowEvalScript(false)) as
part of the CVE-2024-4367 hardening, but the unit test still expected
allowEvalScript(true), failing the equality assertion. Align the
expected ContentSecurityPolicy with the hardened controller behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Comment thread js/workersrc.js
Comment on lines +52 to +53
} catch (e) {
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we want to set the value of enableScripting, so we aren't interested in the error. Anyway, it might be better to include a comment to ensure it's intentional

Comment thread js/workersrc.js
enableScripting = head.getAttribute('data-enableScripting') === 'true';
} catch (e) {
}
PDFViewerApplicationOptions.set('enableScripting', enableScripting);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to move this line with the rest of the PDFViewerApplicationOptions.set(...) sentences.

jvillafanez
jvillafanez previously approved these changes Jul 14, 2026

@DeepDiver1975 DeepDiver1975 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: CVE-2024-4367 FontMatrix code injection

Overview

Backports the CVE-2024-4367 fix into the bundled pdf.js 3.15.2 using defense in depth across four layers: the worker /FontMatrix type check, the glyph-command numeric coercion in pdf.js, the fail-safe isEvalSupported=false ordering + blob-popup detection in workersrc.js, and allowEvalScript(false) in the CSP. The layered approach is well-judged — any single layer holds if the others regress.

I verified the scale special-case is correct: it is the only glyph command that legitimately emits non-numeric args (["size","-size"] at pdf.worker.js:54810); every other emitter (transform, translate, moveTo, …) passes numeric arrays. The worker guard sits at the case 73 join point after all font-loading branches, so it runs on every path.

Strengths

  • Correct root cause + sink coverage. Guard A stops the bad matrix at the source; Guard B independently neutralizes the new Function() sink even if Guard A were bypassed. verify-cve-2024-4367.mjs explicitly tests the "Guard A failed" scenario (B1–B4) — the right adversarial framing.
  • Real bypass fixed, not just the CVE. The #?file=blob hash-fragment observation is a genuine find — the old substring check was satisfiable from the attacker-controlled fragment.
  • Fail-safe ordering. Moving isEvalSupported=false ahead of the throwing locale lookup correctly fixes the swallowed-exception bypass.

Issues & Suggestions

Medium — .mjs security tests aren't wired into CI and duplicate the implementation. tests/security/*.mjs are not referenced by the Makefile or any workflow, so they only run on a manual node ... invocation. They also re-declare copies of sanitizeFontMatrix, buildBody, and isBlobDownloadPopup rather than importing the real code — so the shipped bundle can regress while the tests stay green. These are genuine regression guards for a security fix; they should run automatically, and the README should acknowledge the reproduce-not-exercise drift risk.

Low — isBlobDownloadPopup() can throw in deferredViewerConfig. decodeURIComponent() throws URIError on a malformed %-sequence (e.g. ?file=%). In redirectIfNotDisplayedInFrame the call is inside try/catch and fails safe, but the call in the close-button block (workersrc.js:144) is outside any try. A malformed URL would abort deferredViewerConfig there — after the security options are set (no security impact) but before workerSrc/locale. Wrapping the decodeURIComponent inside isBlobDownloadPopup (return false on throw) makes it robust everywhere.

Low — style nits. workersrc.js still ends without a trailing newline. The intentional empty catch (e) {} blocks would benefit from a one-word // best-effort marker so a future reader doesn't "fix" them.

Security assessment

The core fix is sound and correctly targets the CVE; the three independent browser-side layers plus the CSP change make eval-based glyph injection unreachable through every path I traced. The #?file=blob frame-escape hardening is a valuable bonus. My only substantive concern is test durability: the strongest verification currently doesn't run in CI and can silently drift from the shipped bundle.

Verdict

Approve with minor follow-ups. The security change is correct and well-layered; nothing blocks merge. Recommend wiring the security tests into CI (or documenting them as manual-only + drift-prone) and optionally hardening the decodeURIComponent call. Upgrading pdf.js to ≥ 4.2.67 remains the right long-term follow-up.

🤖 Generated with Claude Code

Address code-review feedback on the CVE-2024-4367 backport:

- Wire the node security checks into CI via a new `test-security` make
  target and a `Security regression checks` job, so they no longer only
  run on a manual invocation.
- Make `verify-cve-2024-4367.mjs` a real gate: it now exits non-zero on
  failure and adds source tripwires (S1-S4) that read the shipped
  pdf.js / pdf.worker.js bundle and fail if the backported guards are
  dropped (e.g. on a future bundle regeneration) - closing the drift
  gap where the reproduced logic could pass while the bundle regressed.
- Harden `isBlobDownloadPopup()` so a malformed %-sequence in the `file`
  query parameter returns false instead of throwing a URIError out of
  `deferredViewerConfig` (the close-button call site was not guarded).
- Annotate the intentional best-effort empty catch blocks and add the
  missing trailing newline in workersrc.js.
- Document the reproduce-not-exercise drift caveat in the security
  README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
DeepDiver1975 and others added 2 commits July 14, 2026 14:59
Pin actions/checkout and actions/setup-node to full commit SHAs (with a
version comment) instead of the movable @v4 major tag, matching the
convention already used in lint-pr-title.yml. A movable tag can be
repointed at malicious code; a SHA cannot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
Bump the SHA pins to the latest release (v7.0.0) of actions/checkout and
actions/setup-node. Dependabot (github-actions ecosystem, already
configured) will keep these current going forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
@DeepDiver1975

Copy link
Copy Markdown
Member Author

Code review

Reviewed at head SHA a57d8fe against the repo and the core CSP classes.

Overview

Solid, well-reasoned backport of the CVE-2024-4367 fix (FontMatrix → new Function() code injection) into the bundled pdf.js 3.15.2, with genuine defense-in-depth across four independent layers:

  1. Worker (pdf.worker.js) — authoritative upstream fix: reject a /FontMatrix that isn't exactly six numbers, falling back to the identity matrix.
  2. Sink (pdf.js) — coerce non-numeric glyph-command args to 0 before interpolation into the generated function body.
  3. Fail-safe eval disabling (workersrc.js) — set isEvalSupported=false first/unconditionally and harden the frame-escape check.
  4. CSP (DisplayController) — allowEvalScript(false) so the sink is unreachable in-browser regardless of the JS.

Verified

  • Action SHAs are correct and current. Both actions/checkout@9c091bb… and actions/setup-node@82076278… resolve to v7.0.0, which is also releases/latest. ✅
  • CSP comment claim is accurate. lib/public/AppFramework/Http/ContentSecurityPolicy.php has protected $evalScriptAllowed = true;, so the controller's ContentSecurityPolicy instance really does default to unsafe-eval. Explicitly calling allowEvalScript(false) (rather than just dropping the old true) is genuinely required. ✅
  • Blob-popup detection matches the real popup format. web/viewer.js opens "?file=" + encodeURIComponent(blobUrl + "#" + filename), so the entire blob:...#name lands in location.search. The new parser decodes file= and checks indexOf('blob:') === 0 — correct, and it fails closed on the attacker #?file=blob hash trick. ✅

Code quality

  • Thoroughly commented with the why — rare and valuable for a security backport. The enableScripting read is isolated so a DOM hiccup can't leave scripting in an unknown state, and the locale lookup (the original swallowed-exception culprit) is now in its own try after the security options are set.
  • The layers are genuinely independent: even if the very first set('isEvalSupported', false) threw, layer 4 (CSP unsafe-eval off) still makes new Function unreachable.
  • The scale-command exception in the sink is safe: scale is emitted with literal ["size","-size"] identifiers, never from font-controlled data.

Minor / non-blocking

  • Test reproductions drift, honestly disclosed. verify-cve-2024-4367.mjs re-declares the guard logic instead of importing the minified bundle; the S1S4 source tripwire mitigates this. S2's regex is brittle to reformatting and could false-fail on any future bundle re-minification even if the guard is intact — worth noting.
  • test-blob-popup-detection.mjs isn't a faithful copy — its local isBlobDownloadPopup omits the shipped try/catch around decodeURIComponent, so a regression in the malformed-%-sequence handling wouldn't be caught. Consider copying that branch or adding a ?file=%E0%A4%A malformed case.
  • PoC PDF is a binary blob — benign payload (documented), but a note on how it was generated (or a generator script) would help future maintainers.
  • Nit: the CVE regression CI job has no needs: dependency; fine functionally.

Risk

Low. Disabling eval doesn't regress rendering (pdf.js falls back to the path-based glyph generator). The one behavioral change worth a manual smoke test is the stricter frame-escape guard: confirm the legitimate attachment popup still opens and a normal PDF renders with eval disabled — the README's E2E step covers this.

🤖 Generated with Claude Code

@DeepDiver1975 DeepDiver1975 merged commit 3358e60 into master Jul 14, 2026
12 checks passed
@DeepDiver1975 DeepDiver1975 deleted the fix/OC10-111-cve-2024-4367 branch July 14, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants