Successor to #8, which established the problem by measurement and is now closed. This issue
is for design, not implementation. #8 proved what the problem is; it did not prove what the
right architecture is, and nothing here should be read as a decided solution.
What #8 established
A 64 KiB PDF makes FilePass inflate 64 MiB of metadata, at roughly 1024:1, linear to at least
256 MiB with no refusal from either library. Four reachable paths, in two libraries:
| # |
path |
triggered by |
phase |
| 1 |
catalog /Metadata XMP |
getMetadata() |
inspection + verification, 2× on the source |
| 2 |
content streams |
getTextContent() |
verification, conditional |
| 3 |
ObjStm |
PDFDocument.load / getDocument |
parsing itself |
| 4 |
XRef stream |
PDFDocument.load / getDocument |
parsing itself |
The only limit FilePass owns today is the 50 MB compressed input cap in sniff.ts, which
does not bound the decompressed size at all.
Equally established, and it constrains the design: the UI does not freeze, the tab does
not crash, and the file is still cleaned and verified correctly. This is CPU and memory on
the user's own device in a local-only tool, not a server-side DoS.
Three separate design questions
They are listed separately because they have different answers and may have different
verdicts. Any of them may reasonably end in "accept as documented limit".
1. Containment of parser decompression
FilePass does not perform the decompression, so it cannot bound it the way it bounds PNG.
Paths 3 and 4 happen during parsing, before FilePass knows anything about the file.
Open, not decided: whether to intercept bytes before any parser sees them, whether to
bound only what is cheaply identifiable, whether a limit can be pushed into the libraries,
whether to bound observationally (time/memory) instead of structurally, or whether to
document the limit and do nothing. Pre-inflating every flate stream ourselves is one
candidate among several and is explicitly not the assumed answer — it is close to
reimplementing part of a PDF parser, which carries its own trust cost.
2. Cancellation and run ownership
Measured: the abandoned run keeps working for 4.6 s after the user has their next answer.
runRef filters results, it does not cancel; there is no AbortController anywhere and
loadingTask.destroy() is never used to interrupt.
Open: what "the user moved on" should mean, whether the run token should own a cancellation
scope, what a partially cancelled verification means for the trust model, and whether
cancellation can be made safe given that a half-finished verification must never produce a
verdict.
3. Concurrency control
Measured: three attack files at once cost 2.91× a single one — fully additive. In the browser
a harmless PDF took 1.4× longer while an abandoned run was still active.
Open: whether PDF work should be serialised, whether a newer run should displace an older
one, and how that interacts with question 2.
Constraints carried over
- No replacement of
pdf-lib or pdfjs-dist merely because they are dependencies.
- No global resource framework built by default; PNG's budget is not to be reused out of
convenience — it works because FilePass holds that reader itself, which is not the case here.
- No change to verification semantics as a side effect.
- A design that concludes "document the limit, change nothing" is a legitimate outcome.
Done when
Each of the three questions has a written verdict — designed, deferred, or accepted as a
documented limit — with the reasoning recorded. Implementation is a separate decision after
that.
Severity
P2. Real and measured, but on the user's own device, with no incorrect sanitisation result
and no fail-open.
🤖 Generated with Claude Code
Successor to #8, which established the problem by measurement and is now closed. This issue
is for design, not implementation. #8 proved what the problem is; it did not prove what the
right architecture is, and nothing here should be read as a decided solution.
What #8 established
A 64 KiB PDF makes FilePass inflate 64 MiB of metadata, at roughly 1024:1, linear to at least
256 MiB with no refusal from either library. Four reachable paths, in two libraries:
/MetadataXMPgetMetadata()getTextContent()PDFDocument.load/getDocumentPDFDocument.load/getDocumentThe only limit FilePass owns today is the 50 MB compressed input cap in
sniff.ts, whichdoes not bound the decompressed size at all.
Equally established, and it constrains the design: the UI does not freeze, the tab does
not crash, and the file is still cleaned and verified correctly. This is CPU and memory on
the user's own device in a local-only tool, not a server-side DoS.
Three separate design questions
They are listed separately because they have different answers and may have different
verdicts. Any of them may reasonably end in "accept as documented limit".
1. Containment of parser decompression
FilePass does not perform the decompression, so it cannot bound it the way it bounds PNG.
Paths 3 and 4 happen during parsing, before FilePass knows anything about the file.
Open, not decided: whether to intercept bytes before any parser sees them, whether to
bound only what is cheaply identifiable, whether a limit can be pushed into the libraries,
whether to bound observationally (time/memory) instead of structurally, or whether to
document the limit and do nothing. Pre-inflating every flate stream ourselves is one
candidate among several and is explicitly not the assumed answer — it is close to
reimplementing part of a PDF parser, which carries its own trust cost.
2. Cancellation and run ownership
Measured: the abandoned run keeps working for 4.6 s after the user has their next answer.
runReffilters results, it does not cancel; there is noAbortControlleranywhere andloadingTask.destroy()is never used to interrupt.Open: what "the user moved on" should mean, whether the run token should own a cancellation
scope, what a partially cancelled verification means for the trust model, and whether
cancellation can be made safe given that a half-finished verification must never produce a
verdict.
3. Concurrency control
Measured: three attack files at once cost 2.91× a single one — fully additive. In the browser
a harmless PDF took 1.4× longer while an abandoned run was still active.
Open: whether PDF work should be serialised, whether a newer run should displace an older
one, and how that interacts with question 2.
Constraints carried over
pdf-liborpdfjs-distmerely because they are dependencies.convenience — it works because FilePass holds that reader itself, which is not the case here.
Done when
Each of the three questions has a written verdict — designed, deferred, or accepted as a
documented limit — with the reasoning recorded. Implementation is a separate decision after
that.
Severity
P2. Real and measured, but on the user's own device, with no incorrect sanitisation result
and no fail-open.
🤖 Generated with Claude Code