Skip to content

fix(file-parsers): guard .doc uploads against zip-bomb memory exhaustion - #6166

Merged
waleedlatif1 merged 4 commits into
stagingfrom
worktree-fix+doc-parser-zip-guard
Aug 2, 2026
Merged

fix(file-parsers): guard .doc uploads against zip-bomb memory exhaustion#6166
waleedlatif1 merged 4 commits into
stagingfrom
worktree-fix+doc-parser-zip-guard

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes a zip-bomb memory-exhaustion path on document upload, in two parts.

1. .doc skipped the guard entirely. DocParser handed the raw upload straight to officeparser and then mammoth, without the assertOoxmlArchiveWithinLimits call its docx/pptx/xlsx siblings all make. The extension is only a routing hint — both libraries accept an OOXML container regardless of its name — so a bomb renamed .doc selected the unguarded parser and bypassed the guard that catches the identical bytes as .docx. The guard is now the first act of DocParser.parseBuffer, and is also enforced centrally in lib/file-parsers parseBuffer so a future parser can't silently opt out.

2. The guard itself was bypassable by lying. The declared uncompressed sizes live in the ZIP central directory, which is attacker-controlled. Under-report them and the size and ratio checks pass untouched; officeparser/mammoth only notice the mismatch after inflating each entry in full. Measured on a 498 KB archive declaring 1000 bytes per entry:

parser before after
.doc +559 MB resident +0 MB, rejected in 2 ms
.docx +538 MB resident +0 MB, rejected in 1 ms
.xlsx / .pptx +11 / +4 MB (container rejected early) +0 MB

Every entry is now inflated during verification under a maxOutputLength bound equal to the size it declared. Node's zlib aborts the moment output would exceed that bound, so a lying entry costs only its declared size and the bytes are discarded immediately. Stored entries are checked against their own compressed size; unsupported compression methods fail closed. Verification walks the contiguous run of central-directory records rather than the EOCD's declared count, since that run is what a decompression library actually allocates per entry — a lied-down count must not hide an entry from verification.

No limits changed: this reuses the existing DEFAULT_OOXML_SIZE_LIMITS all four parsers already ship against.

Type of Change

  • Bug fix (security — resource exhaustion / DoS)

Testing

Both bomb variants (honest and under-declared) are now rejected at +0 MB across .doc, .docx, .xlsx, .pptx, and .txt. Verified the new tests go red when the verification pass is disabled (4 fail) and when the .doc guard call is removed (3 fail).

No false positives, checked against real files rather than only synthetic ones:

  • 17/17 real Word-produced .docx fixtures from mammoth's test data still accepted
  • txt / md / json / yaml / csv / html, valid OOXML as .docx/.doc/.pptx, and legacy OLE .doc all parse unchanged
  • 0/2000 random buffers rejected; PDF, OLE, and BOM-text magic all pass
  • maxOutputLength boundary confirmed to accept output equal to the cap, so honest archives are never rejected

Cost: ~0.45 ms per MB of uncompressed content (1.4 ms @ 1 MB, 5.3 ms @ 10 MB, 22 ms @ 50 MB), against parse times an order of magnitude larger. The declared-size check still runs first, so a straightforward bomb is rejected at zero cost.

Gates: tsc clean, check:api-validation passed, biome clean, 61 file-parser tests plus 1495 tests across uploads / knowledge / files API / copilot. (8 lib/copilot/** test files fail to collect on a PostCSS/Tailwind config error — confirmed identical with this branch's changes fully reverted, so pre-existing and unrelated.)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

DocParser handed the raw upload straight to officeparser and then mammoth,
both of which inflate every ZIP entry into memory before any app-level size
cap applies. The extension is only a routing hint, so a bomb-bearing OOXML
archive renamed to .doc selected the one parser that skipped the guard its
docx/pptx/xlsx siblings all call.

Adds assertOoxmlArchiveWithinLimits to DocParser.parseBuffer, and centrally
in file-parsers parseBuffer so a future parser cannot silently opt out. The
guard reads the central directory's declared sizes without decompressing,
and no-ops for non-ZIP buffers, so legacy OLE .doc files are unaffected.
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 1, 2026 11:56pm

Request Review

@cursor

cursor Bot commented Aug 1, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Security-critical upload parsing and new synchronous inflation on the hot path; incorrect bounds could reject valid Office files or still allow resource exhaustion.

Overview
Closes a zip-bomb memory-exhaustion path on document upload by plugging a missing guard on .doc and strengthening the shared ZIP check so lied central-directory metadata cannot bypass it.

DocParser now calls assertOoxmlArchiveWithinLimits before officeparser/mammoth (same as docx/xlsx/pptx), and the central parseBuffer helper runs the guard for every extension so extension-based routing cannot skip it. Legacy OLE .doc buffers still no-op the guard.

The zip guard adds a second stage after declared-size/ratio checks: each DEFLATE entry is inflated with maxOutputLength tied to its declared size, central vs local compression method and sizes must agree, and central-directory walks charge the contiguous record run (not only the EOCD entry count) so under-reported counts cannot hide large entries.

New tests cover DocParser and expanded zip-guard cases (under-declared sizes, header splits, EOCD count tampering).

Reviewed by Cursor Bugbot for commit bea4e75. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6a006c4. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6a006c4. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

The declared uncompressed sizes in a ZIP central directory are attacker-
controlled, so a bomb can under-report them and pass the size and ratio
checks untouched. officeparser and mammoth only detect the mismatch after
inflating the entry in full: a 498 KB archive declaring 1000 bytes per entry
drove 559 MB resident through the .doc parser and 538 MB through .docx, then
failed. SheetJS and officeparser reject the container earlier, so xlsx/pptx
were not affected, but doc and docx both were.

Each entry is now inflated during verification under a maxOutputLength bound
equal to the size it declared. Node's zlib aborts the moment output would
exceed that bound, so a lying entry costs only its declared size and the
inflated bytes are discarded immediately; both bomb variants now reject at
+0 MB across every extension. Stored entries are checked against their own
compressed size, and unsupported compression methods fail closed.

Verification walks the contiguous run of central-directory records rather
than the EOCD's declared entry count, since that run is what a decompression
library allocates per entry — a lied-down count must not hide an entry from
verification.

Cost is ~0.45 ms per MB of uncompressed content (22 ms for a 50 MB archive),
against parse times an order of magnitude larger. All 17 real Word-produced
.docx fixtures in mammoth's test data are still accepted.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR hardens uploaded-document parsing against ZIP bombs, including archives disguised with non-OOXML extensions and entries that under-report their expanded sizes.

  • Applies archive validation centrally and directly within DocParser.
  • Boundedly inflates DEFLATE entries to verify declared sizes before downstream parsing.
  • Validates central-directory and local-header agreement and walks all contiguous directory records.
  • Adds regression coverage for oversized, under-declared, malformed, and header-mismatched archives.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported central/local compression-method bypass is rejected before payload processing and covered by a regression test.

Important Files Changed

Filename Overview
apps/sim/lib/file-parsers/zip-guard.ts Adds bounded per-entry inflation, central/local header consistency checks, and contiguous central-directory traversal; the previously reported method-mismatch bypass is rejected before payload handling.
apps/sim/lib/file-parsers/zip-guard.test.ts Adds focused regression tests for under-declared sizes, unsupported methods, mismatched headers, stored-entry inconsistencies, and under-reported entry counts.
apps/sim/lib/file-parsers/doc-parser.ts Runs ZIP-bomb validation before either document extraction library while preserving legacy non-ZIP .doc handling.
apps/sim/lib/file-parsers/index.ts Enforces archive validation centrally for all extension-routed buffer parsing.
apps/sim/lib/file-parsers/doc-parser.test.ts Verifies guarded .doc handling rejects malicious ZIP-shaped inputs before downstream libraries and still accepts valid OOXML and legacy OLE documents.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Uploaded buffer] --> B{ZIP-shaped?}
  B -- No --> C[Defer to selected parser]
  B -- Yes --> D[Read contiguous central-directory records]
  D --> E{Declared totals and ratio within limits?}
  E -- No --> R[Reject archive]
  E -- Yes --> F{Central and local headers agree?}
  F -- No --> R
  F -- Yes --> G[Inflate each DEFLATE entry with declared-size output cap]
  G --> H{Actual output matches declaration?}
  H -- No --> R
  H -- Yes --> C
Loading

Reviews (3): Last reviewed commit: "fix(file-parsers): charge hidden central..." | Re-trigger Greptile

Comment thread apps/sim/lib/file-parsers/zip-guard.ts
Comment thread apps/sim/lib/file-parsers/zip-guard.ts
The parsers disagree about which header to trust. JSZip skips the local
header outright and decompresses using the central directory's method, while
SheetJS's parse_local_file switches on the local header's method and inflates
from there. An entry claiming STORED centrally and DEFLATE locally therefore
took the guard's stored branch, skipping bounded inflation, and was still
expanded downstream — a 398 KB archive hiding a 400 MB deflate payload.

Verification now rejects any entry whose two headers disagree on compression
method, and on declared sizes when the local header carries them (the
data-descriptor flag and ZIP64 sentinels legitimately omit them, and those
entries stay covered by the bounded inflate).

Caught by Greptile review. All 17 real Word-produced .docx fixtures in
mammoth's test data are still accepted.
…e cap

sumDeclaredUncompressedSize walked only the entry count the EOCD declares,
while verification walks the contiguous run of records. JSZip's readCentralDir
loops on the record signature and keeps every entry it finds — a count
mismatch is explicitly not an error there — so an archive that under-reported
its count could hide honestly-large entries from the total-size cap and still
have the parser expand them.

The sum now walks the same contiguous run as the verification pass and
readZipCentralDirectoryStats, and fails closed when the run is shorter than
the declared count.

Caught by Cursor Bugbot review.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit bea4e75. Configure here.

@waleedlatif1
waleedlatif1 merged commit ccc2ec9 into staging Aug 2, 2026
27 checks passed
@waleedlatif1
waleedlatif1 deleted the worktree-fix+doc-parser-zip-guard branch August 2, 2026 00:30
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.

1 participant