Repository navigation
fix: limit PDF decompression on load and drop invisible characters from drafts - #206
Conversation
jerelvelarde
left a comment
There was a problem hiding this comment.
Reviewed dd68b41adf4dd85fb6e3e51515562b9ff564995e.
Bounds decompression during PDF loads and normalizes hidden draft controls at the shared schema boundary. Both changes address untrusted input; no dependencies or deployment changes. Security: traced upload/import/fill through loadPdf, per-load AsyncLocalStorage accounting and parser-error recovery; action proposal/review/send share the normalization. No actionable security finding. The decoder still allocates before charging its growth, so this is a bounded rejection threshold, not a strict 100 MiB process-memory ceiling; concurrent loads and other PDF parsing allocations remain outside that claim. Verification: 22 PDF/domain tests passed, including an oversized compressed object stream concurrent with an ordinary PDF. Native devices and live provider imports were not exercised.
Local execution used read-only, network-disabled Linux containers with synthetic data and Node 24.17.0. Root dependency fixtures came from main’s manifest rather than the lockfile; exact-head GitHub CI covers locked full checks where green. No production credentials were supplied.
Closes #205
What
Bounded PDF decompression.
loadPdfnow limits each load to 100 MiB of decompressed output (10× the file size limit), and rejects the file with a 422 when it goes over. Before, pdf-lib decompressed object and cross-reference streams while loading, with no limit on output, so a file under 10 MB could use unbounded memory while it was checked.DecodeStream.ensureBuffer, sopdf.tswraps that one method and charges each buffer's growth to the current load. The limit is per load: it's carried inAsyncLocalStorage, so concurrent imports don't share it.loadPdftherefore checks the remaining budget both after a successful load and in its catch.inspectPdfandfillPdfboth go throughloadPdf, so upload, Gmail attachment import, browser download import and computer export are all covered.Invisible characters in drafts. The email
subject/bodyand the eventtitle/location/descriptionschemas now drop bidi embedding/override/isolate controls, zero-width space, word joiner, invisible operators, BOM and tag characters. This is done with.overwrite(), so the field stays a plain string. Because the schemas are shared by the model tools,POST /api/actions, the review card and the Gmail/Calendar send path, what the review card shows is what gets sent. Zero-width joiners and non-joiners and the LRM/RLM marks are kept, because emoji, Persian and Arabic text use them.Tests
tests/pdf.test.ts: a valid one-page PDF with an object stream that decompresses past the limit is rejected with the limit error, while a sample PDF loaded at the same time still succeeds. Against the previouspdf.ts, the oversized file loads successfully.tests/domain.test.ts: invisible characters are dropped from every free-text draft field, and a subject made only of them is rejected (fails against the previous schema). Text with joiners and directional marks is kept unchanged.Tested
pnpm lint,pnpm typecheck,pnpm --dir apps/worker typecheck,pnpm build:serverpnpm test: 512 passed, 0 failed