fix(storage): bound tar imports, contain storage keys across backends, stop export-file accumulation#346
Closed
rmyndharis wants to merge 1 commit into
Closed
fix(storage): bound tar imports, contain storage keys across backends, stop export-file accumulation#346rmyndharis wants to merge 1 commit into
rmyndharis wants to merge 1 commit into
Conversation
…, stop export-file accumulation - A tar.gz import buffered each entry with no size or count limit (decompression-bomb DoS). Imports now enforce a per-entry byte cap (STORAGE_IMPORT_MAX_BYTES, default 200 MiB) and a max entry count (STORAGE_IMPORT_MAX_ENTRIES, default 100000), aborting the whole import on breach. - Storage-key containment is now enforced at the backend-agnostic get/putFile boundary (isSafeStorageKey: rejects '..' traversal, absolute paths, and NUL/control chars), so the S3 object key inherits the guard the local path already had. - A plugin's ctx.storage get/set/delete built a path from the raw key, so a '..' key could escape the plugin's sandbox dir; keys that escape are now rejected (JID-style keys preserved). - GET /infra/storage/export wrote a timestamped tar.gz of all media into data/ and never deleted it (unbounded disk growth on the volume that holds the live DBs). The export now writes under data/exports/ (kept inside the import-allowed root so the documented backend-migration roundtrip still works across a restart) with a collision-proof filename, is auto-removed after STORAGE_EXPORT_TTL_MS (default 1h), and reads each file asynchronously instead of blocking the event loop.
Merged
rmyndharis
added a commit
that referenced
this pull request
Jun 19, 2026
* fix(webhook): deliver session lifecycle events and key webhook hardening (#335) * fix(security): pin outbound webhook and media fetches to validated IP (#338) * fix(plugins): persist plugin enable/config and restore (#339) * fix(message): persist bulk-sent messages, sanitize SSRF (#340) * fix(engine): return the real id for forwarded messages (#341) * fix(security): harden outbound requests, IPv6 SSRF (#344) * fix(security): secret-file perms, key pepper, allowedIps (#345) * fix(storage): bound tar imports, contain storage keys (#346) * fix(session): reconcile late acks, serialize reactions (#348) * fix(contract): webhook timeout, bounded shutdown, 501 (#350) * feat(session): force-kill a stuck session (#352) * merge #343 * merge #351 * chore(release): v0.4.3 — CHANGELOG, version bump (package.json/dashboard/swagger), README + docs
Owner
Author
|
Shipped in v0.4.3 (integrated via the release PR #354 and tagged |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four small storage import/export hardening fixes (grouped — all on the
/infra/storage/*resource path). All ADMIN-gated; no wire-format change.1. Tar import is bounded against decompression bombs
importFromStreambuffered each tar entry into memory with no size limit and no entry-count limit, so a crafted.tar.gz(small archive, one entry expanding to GBs, or millions of entries) could exhaust process memory. Imports now enforce:STORAGE_IMPORT_MAX_BYTES(default 200 MiB), checked on the running accumulator so it aborts mid-stream, not after bufferingSTORAGE_IMPORT_MAX_ENTRIES(default 100000)On breach the whole import is torn down and rejected. A single bad/traversing entry is still skipped-and-continued (unchanged); import remains best-effort/non-atomic (documented in-code).
2. Storage-key containment now covers the S3 backend
The local backend rejected tar entry names that traversed the storage root; the S3 path (
media/${key}) had no such guard. Containment is now checked at the backend-agnosticgetFile/putFileboundary viaisSafeStorageKey— rejecting..traversal, absolute paths, and NUL/control characters (which are harmless on the local FS but would reach the raw S3 object key). Ordinary media keys are unaffected.3. Plugin storage is sandbox-contained
A plugin's
ctx.storageget/set/delete built a file path from the raw key, so a key containing..could read/write/delete outside the plugin's own data directory (clobber the registry, another plugin's data, or.env.generated). Keys that escape the sandbox are now rejected; JID-style keys (group:sid:chat@g.us) are preserved.4. Storage export no longer accumulates on the data volume
GET /infra/storage/exportwrote a timestampedtar.gzof all media intodata/and never deleted it, so repeated exports grew without bound on the same volume that holds the live SQLite DBs + session state (a full disk corrupts the gateway). The export now:data/exports/with a collision-proofDate.now()-${uuid}filenameSTORAGE_EXPORT_TTL_MS, default 1h)fs.promises.readFileinstead of a synchronous read that blocked the event loop per fileTests
New/updated unit tests: import per-entry + entry-count caps (incl. a multi-chunk 256 KiB entry proving mid-stream abort);
isSafeStorageKey(traversal/absolute/NUL/control + JID-key acceptance);get/putFilereject an unsafe key before any S3 call (S3 inherits the guard); plugin-storage refuses a traversing get even when a real file exists at the target, and rejects traversing set/delete; export lands underdata/exports, stays import-able, and is TTL-swept (poll-based assertion, not a fixed sleep). Full backend 752/752; dashboard build + lint clean.