fix: mitigate CVE-2024-4367 FontMatrix code injection in pdf viewer#351
Conversation
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>
f2a2219 to
028c71e
Compare
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>
| } catch (e) { | ||
| } |
There was a problem hiding this comment.
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
| enableScripting = head.getAttribute('data-enableScripting') === 'true'; | ||
| } catch (e) { | ||
| } | ||
| PDFViewerApplicationOptions.set('enableScripting', enableScripting); |
There was a problem hiding this comment.
It might be better to move this line with the rest of the PDFViewerApplicationOptions.set(...) sentences.
DeepDiver1975
left a comment
There was a problem hiding this comment.
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.mjsexplicitly tests the "Guard A failed" scenario (B1–B4) — the right adversarial framing. - Real bypass fixed, not just the CVE. The
#?file=blobhash-fragment observation is a genuine find — the old substring check was satisfiable from the attacker-controlled fragment. - Fail-safe ordering. Moving
isEvalSupported=falseahead 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>
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>
Code reviewReviewed at head SHA OverviewSolid, well-reasoned backport of the CVE-2024-4367 fix (FontMatrix →
Verified
Code quality
Minor / non-blocking
RiskLow. 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 |
Summary
Mitigates CVE-2024-4367 (FontMatrix code injection) in the bundled pdf.js. A missing type check on the PDF
/FontMatrixattribute allowed an attacker-controlled string to flow intonew Function()in the glyph renderer, executing JavaScript in the viewer origin (XSS → account takeover through a public share / attachment popup).The pre-existing
isEvalSupported=falsehardening was bypassable: inworkersrc.jsit was set after a locale lookup (parent.OC.getLocale()) inside a singletrywith a barecatch. When the viewer runs top-level (the?file=blobpopup)parent.OCcan throw, thecatchswallows it, and eval stays enabled. The viewer CSP also explicitly allowedunsafe-eval.Changes (defense in depth)
pdf.worker.js— backport the upstream type check: a/FontMatrixthat 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 intonew Function()(the intentionalscaleidentifiers are preserved).workersrc.js— setisEvalSupported=falseandenableScriptingfirst and unconditionally; isolate the throwing locale lookup; detect the blob popup by parsing thefilequery parameter for ablob:URL instead of a looseindexOf('?file=blob')substring match (which was satisfiable from the attacker-controlled URL hash).DisplayController— dropallowEvalScript(true)so thenew Functionsink 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