Skip to content

fix(pdf): upgrade pdfjs-dist past GHSA-hq66-cqwq-w95j (5.7.284 → 6.3.289) - #124

Merged
mrsibe merged 1 commit into
mainfrom
fix/pdfjs-remediate
Sep 25, 2026
Merged

mrsibe merged 1 commit into
mainfrom
fix/pdfjs-remediate

Conversation

@mrsibe

@mrsibe mrsibe commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Closes #121.

pdfjs-dist resolved to 5.7.284, inside the affected range of GHSA-hq66-cqwq-w95j / CVE-2026-16633 (>=5.6.83 <6.2.108, fixed 6.2.108). Untrusted documents are this app's normal input, so a known-high in that path is a v1.4 release blocker.

Reachability: the current exploit path is not reachable under the inspected call sites

That wording is deliberate — it is a statement about our code, not a claim that the library is safe. The distinction is the whole reason this is worth writing down.

  • Both paths call getDocument with no scripting option, and neither constructs an AnnotationLayer nor imports pdfjs-dist/web/*.
  • PDFScriptingManager — the component that actually executes document JavaScript — exists only in the viewer build. grep -c PDFScriptingManager against the API build build/pdf.mjs is 0, and bare pdfjs-dist resolves to main: build/pdf.mjs. The only other pdfjs import in the repo is the parsing worker.
  • In the API build the annotation layer's _bindJSAction only binds DOM listeners that dispatch into a sandbox the viewer is expected to provide.

So the path is not reachable today — because of what we do or don't construct.

Correction to this issue's own acceptance

Item 2 asked for enableScripting: false to be passed explicitly on both getDocument calls. That is not implementable, and I did not force it:

src/main/services/loaders/PdfLoader.ts: error TS2339 / TS2353:
  'enableScripting' does not exist in type 'DocumentInitParameters'

enableScripting is declared on AnnotationLayer, AnnotationLayerBuilder and PDFViewer — it is not a getDocument parameter. The advisory's workaround ("set enableScripting to false or set a CSP") targets the viewer, which this app does not use. There is no switch to flip at our call sites.

So the mitigation is structural, and it is now a test instead of a one-time inspection: test/pdfjsScriptingBoundary.test.ts fails if any source file

  • imports the viewer build (pdfjs-dist/web/* / pdf_viewer), where the scripting manager lives,
  • constructs AnnotationLayer / AnnotationLayerBuilder / PDFScriptingManager / ScriptingManager, or
  • sets enableScripting: true.

It carries a self-check (the guard actually scans the source tree) so it cannot pass by scanning nothing, and I confirmed it fails when a new AnnotationLayer() probe is injected. That is what turns "we happen not to do this" into a CI-enforced invariant — and it is what makes the "not reachable" conclusion above durable rather than historical.

The CSP item is already satisfied

The issue asked whether a CSP is additionally warranted for the reader. It is already in place, in src/renderer/index.html:

default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; ... worker-src 'self' blob:;

script-src 'self' with no 'unsafe-inline' and no 'unsafe-eval'. The advisory requires "no CSP for disallowing script-src"; that condition is not met. So the renderer — the process that renders untrusted documents — has two independent barriers: no scripting machinery is constructed, and injected script would be blocked by CSP anyway.

The upgrade: 5.7.284 → 6.3.289

The entire API breakage is one moved method. destroy() is gone from PDFDocumentProxy (which keeps cleanup()) and now lives on PDFDocumentLoadingTask. Three mechanical call sites:

Site Change
PdfLoader.ts destroy the loading task it already holds
PdfSourceReader.tsx hold the loading task for teardown instead of the document; clear it before destroying on the cancelled path so unmount cannot destroy the same task twice

Nothing else broke. engines on 6.x is >=22.13.0 \|\| >=24; CI and CONTRIBUTING both pin Node 24.

Rollback plan

Mechanically two steps: restore ^5.x, revert the three destroy() call sites.

It is deliberately not the default, because it re-opens this issue — the version would be inside a known-high range again. The structural guard and the CSP survive a rollback, so the residual risk is specifically "a known-high version ships", which is what the release policy forbids. A rollback therefore needs an explicit maintainer decision and a note in the release, not a quiet revert.

Acceptance

Criterion Status
Exploit conditions re-confirmed on both paths, recorded Done — above, and readable in the code comments at both call sites
enableScripting: false on both getDocument calls Not implementable — corrected above; replaced by the structural boundary guard
CSP evaluation for the reader Already present and sufficient — quoted above
Upgrade to >=6.2.108 Done — 6.3.289
Reader regression (render, text layer, bbox highlight, worker, zoom) Manual QA — see below
main PdfLoader regression Covered — packaged smoke test parses the fixture; parser tests pass
Packaged artifact on Windows and macOS CI on this PR
npm audit clean for pdfjs-dist Done — found 0 vulnerabilities
Rollback plan recorded Done — above

Verification

  • npm run typecheck — all three projects
  • npm test — 258 pass / 0 fail (4 new: the boundary guard)
  • npm run build:unpack && npm run smoke:packaged — executed on the packaged app, 20/20 checks, including pdfjs-dist loads and polyfilled DOMMatrix (the historical startup crash point) and a real PDF import extracts text fixture parse under 6.3.289:
    [SmokeTest] ok   pdfjs-dist loads and polyfilled DOMMatrix from @napi-rs/canvas
    [SmokeTest] ok   PDF import extracts text (pdfjs-dist)
    [SmokeTest] PASS - 20 checks passed
    
  • npm audit --omit=dev --registry=https://registry.npmjs.org — found 0 vulnerabilities
  • npm run check:design, npm run lint (0 errors, 107 warning baseline unchanged), npx prettier --check

Not verified

The reader's rendering path — canvas render, text-layer selection (#73 excerpts depend on it), bbox highlighting, worker loading, zoom — needs a real window and remains manual QA. Typecheck and the packaged build cover it structurally, and PdfSourceReader had to change for the destroy() move, so this is the one place worth a human pass before release. CI covers the packaged main-process parse on Windows and macOS.

isEvalSupported (font-data eval) is a different vector from this CVE and is not addressed here.

…cripting boundary

Closes #121. `pdfjs-dist` was resolved to 5.7.284, inside the affected range of
GHSA-hq66-cqwq-w95j / CVE-2026-16633 (`>=5.6.83 <6.2.108`, fixed 6.2.108):
arbitrary JavaScript execution upon opening a malicious PDF. Untrusted documents are
this app's normal input, so a known-high in that path is a release blocker.

**The reachability finding, stated precisely: the current exploit path is not
reachable under the inspected call sites.** That is a statement about our code, not
about the library, and it is now enforced rather than assumed - see the guard below.

- main (`PdfLoader`) and renderer (`PdfSourceReader`) both call `getDocument` with no
  scripting option, and neither constructs an `AnnotationLayer` nor imports
  `pdfjs-dist/web/*`. `PDFScriptingManager`, which is what actually executes document
  JavaScript, exists only in the viewer build; it is absent from the API build
  entirely (`grep -c PDFScriptingManager build/pdf.mjs` is 0).
- The renderer additionally already carries a CSP with `script-src 'self'` and
  neither `'unsafe-inline'` nor `'unsafe-eval'` (`src/renderer/index.html`), so the
  advisory's second prerequisite - "no CSP for disallowing script-src" - is not met
  either. Two independent barriers on the path that renders untrusted documents.

**Correction to the issue's own acceptance.** It asked for `enableScripting: false`
to be passed explicitly on both `getDocument` calls. That is not implementable:
`enableScripting` is declared on `AnnotationLayer` / `AnnotationLayerBuilder` /
`PDFViewer`, and is **not** a `DocumentInitParameters` member - TypeScript rejects it
on `getDocument`. The advisory's workaround targets the viewer, which this app does
not use. The honest mitigation is therefore structural, and it is now a test:
`test/pdfjsScriptingBoundary.test.ts` fails if any source file imports the viewer
build, constructs the annotation layer or scripting manager, or sets
`enableScripting: true`. That converts "we happen not to do this" into a CI-enforced
invariant, and it carries a self-check so it cannot silently scan nothing.

**The upgrade: 5.7.284 -> 6.3.289.** The whole API breakage is one moved method:
`destroy()` is gone from `PDFDocumentProxy` (which keeps `cleanup()`) and now lives on
`PDFDocumentLoadingTask`. Three call sites, all mechanical:

- `PdfLoader.ts` destroys the loading task it already holds.
- `PdfSourceReader.tsx` now keeps the loading task for teardown instead of the
  document, and clears it before destroying on the cancelled path so unmount cannot
  destroy the same task twice.

`engines` on 6.x is `>=22.13.0 || >=24`; CI and CONTRIBUTING both pin Node 24.

**Rollback plan.** Mechanically it is two steps: restore `^5.x` and revert the three
`destroy()` call sites. It is deliberately not the default, because it re-opens this
issue: the version would be inside a known-high range again. The structural mitigation
and the CSP survive a rollback, so the residual risk is specifically "a known-high
version ships", which is the thing the release policy forbids - so a rollback needs an
explicit maintainer decision and a note in the release, not a quiet revert.

Verification: `npm run typecheck` (all three projects), `npm test` (258 pass, 4 new for
the boundary guard), `npm run build:unpack && npm run smoke:packaged` **executed on the
packaged app** - 20/20 checks, including `pdfjs-dist loads and polyfilled DOMMatrix`
and a real `PDF import extracts text` fixture parse under 6.3.289 - and
`npm audit --omit=dev --registry=https://registry.npmjs.org` now reports **0
vulnerabilities**. `npm run check:design`, `npm run lint` (0 errors, baseline
unchanged), `npx prettier --check`.

Not verified: the reader's rendering path (canvas render, text-layer selection,
bbox highlighting, zoom) needs a real window and is manual QA; CI covers the packaged
main-process parse on Windows and macOS.
@github-actions github-actions Bot added the bug Something isn't working label Sep 25, 2026
@mrsibe
mrsibe merged commit 6c26628 into main Sep 25, 2026
4 checks passed
mrsibe added a commit that referenced this pull request Sep 25, 2026
`pdfjs-dist` 6.3.289 calls `Map.prototype.getOrInsertComputed` (and
`getOrInsert`) throughout the API build and the worker, but those shipped in
Chromium 145 and Electron 39 is Chromium 142. The renderer imported the modern
build, so the first `page.render()` threw

    TypeError: this[#methodPromises].getOrInsertComputed is not a function

from `WorkerTransport.getOptionalContentConfig`, and every page painted blank.
The main-process loader already imports `pdfjs-dist/legacy/build/pdf.mjs`, whose
core-js polyfills install those methods, so text extraction was unaffected while
the reader was broken.

Point the reader and its worker at the same legacy build. Switch back to
`pdfjs-dist/build/*` once the app runs on Chromium >= 145.

The reader path slipped through #124 (the upgrade for #121), whose own message
records the rendering path as unverified manual QA. `test/pdfjsScriptingBoundary.test.ts`
now fails if any source file imports the modern build, and its self-check accepts
the legacy subpath.

Verified: `npm test` (271 pass), `npm run typecheck`, `npm run build`; the built
`out/renderer/assets/pdf.worker.min-*.mjs` carries the polyfill
(`target:"Map",proto:!0`) and the renderer bundle references that legacy worker.
The new guard was falsified by temporarily restoring the modern import, which
fails it as intended.
Not verified: canvas render, text-layer selection, bbox highlighting and zoom
under a real window - that needs manual QA, and is #125's job.
@mrsibe
mrsibe deleted the fix/pdfjs-remediate branch September 25, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(pdf): remediate GHSA-hq66-cqwq-w95j before v1.4 (pdfjs-dist 5.7.284 in affected range)

1 participant