Skip to content

💣 fix: Harden against falsified ZIP metadata in ODT parsing#12320

Merged
danny-avila merged 4 commits into
devfrom
fix/odt-zip-bomb-guard
Mar 20, 2026
Merged

💣 fix: Harden against falsified ZIP metadata in ODT parsing#12320
danny-avila merged 4 commits into
devfrom
fix/odt-zip-bomb-guard

Conversation

@danny-avila

@danny-avila danny-avila commented Mar 20, 2026

Copy link
Copy Markdown
Owner

Summary

I replaced the ODT parser's JSZip-based decompression with yauzl's streaming API to close a medium-severity DoS where an authenticated user could bypass the 50MB size guard by falsifying ZIP central directory metadata, causing content.xml to expand unboundedly into heap memory.

  • Replaces jszip with yauzl as the production dependency for ODT parsing, removing the previous approach of loading the entire archive into memory via JSZip.loadAsync
  • Extracts ZIP handling into a dedicated extractOdtContentXml() helper that uses yauzl.open with lazyEntries: true, streaming content.xml via openReadStream and counting real decompressed bytes as they arrive from the inflate stream — not from attacker-controlled ZIP central directory fields
  • Aborts decompression mid-inflate by calling readStream.destroy(err) the moment the real byte count exceeds ODT_MAX_DECOMPRESSED_SIZE (50MB), preventing the full allocation from ever completing
  • Introduces a finish() helper guarded by a settled flag to ensure zipfile.close() is called exactly once on every exit path — success, size cap breach, missing content.xml, openReadStream errors, and zipfile-level errors — eliminating file descriptor leaks on all error paths
  • Adds jszip to devDependencies so the zip-bomb test fixture, which constructs a crafted DEFLATE-compressed ODT in-memory, has an explicit declared dependency rather than relying on a transitive one from mammoth
  • Adds @types/yauzl to devDependencies for TypeScript declarations, as yauzl@3 ships no bundled .d.ts files
  • Updates the extractOdtContentXml JSDoc to document both the metadata-bypass protection and the FD lifecycle guarantee

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

The existing zip-bomb test in packages/api/src/files/documents/crud.spec.ts was updated to reflect the new guard behavior. It constructs a DEFLATE-compressed ODT in-memory (~51KB on disk, 51MB uncompressed) using JSZip, writes it to a temp file, and asserts the streaming guard rejects with /exceeds the 50MB decompressed limit/ before the full content is allocated. Cleanup runs in a finally block regardless of outcome.

All existing ODT parser tests (sample.odt, empty.odt, sample-entities.odt) continue to pass against the yauzl-based implementation.

cd packages/api
npx jest --testPathPatterns=crud.spec --no-coverage

Test Configuration:

No environment variables or external services required.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

…sion

The ODT decompressed-size guard was checking JSZip's private
_data.uncompressedSize fields, which are populated from the ZIP central
directory — attacker-controlled metadata. A crafted ODT with falsified
uncompressedSize values bypassed the 50MB cap entirely, allowing
content.xml decompression to exhaust Node.js heap memory (DoS).

Replace JSZip with yauzl for ODT extraction. The new extractOdtContentXml
function uses yauzl's streaming API: it lazily iterates ZIP entries,
opens a decompression stream for content.xml, and counts real bytes as
they arrive from the inflate stream. The stream is destroyed the moment
the byte count crosses ODT_MAX_DECOMPRESSED_SIZE, aborting the inflate
before the full payload is materialised in memory.

- Remove jszip from direct dependencies (still transitive via mammoth)
- Add yauzl + @types/yauzl
- Update zip-bomb test to verify streaming abort with DEFLATE payload
Copilot AI review requested due to automatic review settings March 20, 2026 01:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens ODT parsing against zip-bomb style DoS by switching from metadata-based decompressed-size checks (attacker-controlled) to streaming decompression with real byte counting and early abort once the 50MB cap is exceeded.

Changes:

  • Replaced JSZip-based ODT content.xml extraction with yauzl streaming extraction and on-the-fly decompressed byte counting.
  • Updated/added a Jest test asserting decompression abort behavior when content.xml exceeds the limit.
  • Swapped jszip for yauzl in @librechat/api dependencies and added @types/yauzl.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
packages/api/src/files/documents/crud.ts Streams content.xml out of ODTs using yauzl and aborts mid-inflate after exceeding the decompressed-size cap.
packages/api/src/files/documents/crud.spec.ts Updates the ODT zip-bomb test expectation/message to match the new streaming guard behavior.
packages/api/package.json Adds yauzl + typings; removes jszip from runtime deps.
package-lock.json Lockfile updates reflecting dependency changes (yauzl added, jszip removed as direct dep).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/api/src/files/documents/crud.ts
Comment thread packages/api/src/files/documents/crud.ts Outdated
Comment thread packages/api/package.json
Comment thread packages/api/src/files/documents/crud.ts
@danny-avila danny-avila changed the title security: fix ODT zip-bomb DoS via falsified ZIP metadata fix: Prevent falsified ZIP metadata in ODT parsing Mar 20, 2026
- Use a shared `finish()` helper in extractOdtContentXml that calls
  zipfile.close() on every exit path (success, size cap, missing entry,
  openReadStream errors, zipfile errors). Without this, any error path
  leaked one OS file descriptor permanently — uploading many malformed
  ODTs could exhaust the process FD limit (a distinct DoS vector).
- Add jszip to devDependencies so the zip-bomb test has an explicit
  dependency rather than relying on mammoth's transitive jszip.
- Update JSDoc to document that all exit paths close the zipfile.
@danny-avila danny-avila changed the title fix: Prevent falsified ZIP metadata in ODT parsing 💣 fix: Harden against falsified ZIP metadata in ODT parsing Mar 20, 2026
Matches the established pattern for runtime parser libraries in
packages/api: mammoth, pdfjs-dist, and xlsx are all peerDependencies
(provided by the consuming /api workspace) with devDependencies for
testing. yauzl was incorrectly placed in dependencies.
packages/api declares yauzl as a peerDependency; /api is the consuming
workspace that must provide it at runtime, matching the pattern used
for mammoth, pdfjs-dist, and xlsx.
@danny-avila
danny-avila merged commit e442984 into dev Mar 20, 2026
9 checks passed
@danny-avila
danny-avila deleted the fix/odt-zip-bomb-guard branch March 20, 2026 02:13
jcbartle pushed a commit to jcbartle/LibreChat that referenced this pull request May 11, 2026
…ila#12320)

* security: replace JSZip metadata guard with yauzl streaming decompression

The ODT decompressed-size guard was checking JSZip's private
_data.uncompressedSize fields, which are populated from the ZIP central
directory — attacker-controlled metadata. A crafted ODT with falsified
uncompressedSize values bypassed the 50MB cap entirely, allowing
content.xml decompression to exhaust Node.js heap memory (DoS).

Replace JSZip with yauzl for ODT extraction. The new extractOdtContentXml
function uses yauzl's streaming API: it lazily iterates ZIP entries,
opens a decompression stream for content.xml, and counts real bytes as
they arrive from the inflate stream. The stream is destroyed the moment
the byte count crosses ODT_MAX_DECOMPRESSED_SIZE, aborting the inflate
before the full payload is materialised in memory.

- Remove jszip from direct dependencies (still transitive via mammoth)
- Add yauzl + @types/yauzl
- Update zip-bomb test to verify streaming abort with DEFLATE payload

* fix: close file descriptor leaks and declare jszip test dependency

- Use a shared `finish()` helper in extractOdtContentXml that calls
  zipfile.close() on every exit path (success, size cap, missing entry,
  openReadStream errors, zipfile errors). Without this, any error path
  leaked one OS file descriptor permanently — uploading many malformed
  ODTs could exhaust the process FD limit (a distinct DoS vector).
- Add jszip to devDependencies so the zip-bomb test has an explicit
  dependency rather than relying on mammoth's transitive jszip.
- Update JSDoc to document that all exit paths close the zipfile.

* fix: move yauzl from dependencies to peerDependencies

Matches the established pattern for runtime parser libraries in
packages/api: mammoth, pdfjs-dist, and xlsx are all peerDependencies
(provided by the consuming /api workspace) with devDependencies for
testing. yauzl was incorrectly placed in dependencies.

* fix: add yauzl to /api dependencies to satisfy peer dep

packages/api declares yauzl as a peerDependency; /api is the consuming
workspace that must provide it at runtime, matching the pattern used
for mammoth, pdfjs-dist, and xlsx.
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…ila#12320)

* security: replace JSZip metadata guard with yauzl streaming decompression

The ODT decompressed-size guard was checking JSZip's private
_data.uncompressedSize fields, which are populated from the ZIP central
directory — attacker-controlled metadata. A crafted ODT with falsified
uncompressedSize values bypassed the 50MB cap entirely, allowing
content.xml decompression to exhaust Node.js heap memory (DoS).

Replace JSZip with yauzl for ODT extraction. The new extractOdtContentXml
function uses yauzl's streaming API: it lazily iterates ZIP entries,
opens a decompression stream for content.xml, and counts real bytes as
they arrive from the inflate stream. The stream is destroyed the moment
the byte count crosses ODT_MAX_DECOMPRESSED_SIZE, aborting the inflate
before the full payload is materialised in memory.

- Remove jszip from direct dependencies (still transitive via mammoth)
- Add yauzl + @types/yauzl
- Update zip-bomb test to verify streaming abort with DEFLATE payload

* fix: close file descriptor leaks and declare jszip test dependency

- Use a shared `finish()` helper in extractOdtContentXml that calls
  zipfile.close() on every exit path (success, size cap, missing entry,
  openReadStream errors, zipfile errors). Without this, any error path
  leaked one OS file descriptor permanently — uploading many malformed
  ODTs could exhaust the process FD limit (a distinct DoS vector).
- Add jszip to devDependencies so the zip-bomb test has an explicit
  dependency rather than relying on mammoth's transitive jszip.
- Update JSDoc to document that all exit paths close the zipfile.

* fix: move yauzl from dependencies to peerDependencies

Matches the established pattern for runtime parser libraries in
packages/api: mammoth, pdfjs-dist, and xlsx are all peerDependencies
(provided by the consuming /api workspace) with devDependencies for
testing. yauzl was incorrectly placed in dependencies.

* fix: add yauzl to /api dependencies to satisfy peer dep

packages/api declares yauzl as a peerDependency; /api is the consuming
workspace that must provide it at runtime, matching the pattern used
for mammoth, pdfjs-dist, and xlsx.
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.

2 participants