fix(security): cap bsdtar extraction size to prevent decompression bomb DoS [DEVA11Y-484]#25
Open
maunilm wants to merge 3 commits into
Open
fix(security): cap bsdtar extraction size to prevent decompression bomb DoS [DEVA11Y-484]#25maunilm wants to merge 3 commits into
maunilm wants to merge 3 commits into
Conversation
…mb DoS [DEVA11Y-484] CWE-400 / OWASP A05. bsdtar was invoked with no decompressed-size or entry-count limit in both the Swift SPM plugin and the bash/zsh/fish CLI wrappers, so an attacker who can influence the download URL (the HTTPS-only --download-url / BROWSERSTACK_A11Y_CLI_DOWNLOAD_URL override, or TLS interception) could serve a decompression bomb that exhausts the developer/CI disk. Swift plugin (BrowserStackAccessibilityLint.swift): - curl now passes --max-filesize (100 MB) to cap the compressed download. - A background watchdog terminates bsdtar once the *decompressed* footprint on disk exceeds 200 MB (a pipe-level cap would only bound compressed bytes, which is useless against a bomb). Applied to both the remote and local extraction paths. - locateExecutable now bounds enumeration at 10,000 entries. Shell wrappers (bash/zsh/fish cli.sh): - curl --max-filesize caps the compressed download. - bsdtar output is piped through `head -c` (200 MB) with pipefail so an oversized archive aborts instead of filling the disk. Real CLI artifact is ~34 MB compressed / ~64 MB decompressed, so the caps leave ~3x headroom and do not affect legitimate downloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on guard [DEVA11Y-484] Adds local integration tests (no mocks) that exercise the decompression-bomb guards against real curl/bsdtar/head and the real Swift watchdog, plus hardens the guard itself based on what the tests surfaced. Guard hardening (Plugins/BrowserStackAccessibilityLint.swift): - The watchdog now also terminates bsdtar on an entry-count ceiling, closing the "millions of tiny files" bomb that stays small on disk (previously only locateExecutable caught it, after the fact). - Added a post-extraction footprint check so detection is deterministic on fast disks: a bomb that finishes decompressing within a single 200ms poll interval is now caught and cleaned up rather than slipping past the live watchdog. - Refactored the guard into a self-contained, marked block of free functions so it can be mirrored and drift-checked. Tests (scripts/test/, run via run_tests.sh): - Shell: extracts the REAL download_binary from bash/zsh/fish verbatim and runs it against a local server (only the hardcoded URL is redirected, via a curl shim). - Swift: a mirror harness compiles the guard block verbatim and drives real curl/bsdtar; check_drift.sh fails CI if the mirror diverges from the plugin (SwiftPM command plugins can't be imported by a test target). - Scenarios: legit (downloads/extracts/runs), 400MB bomb, 20k-entry bomb, oversized (>100MB) download, corrupt archive, multi-file, missing URL. - Fixtures are bounded (≤400MB, gitignored) and bomb tests use a small cap, so a regressed guard can never exhaust the disk. Full run ~9s, disk usage flat. - CI: .github/workflows/extraction-guard-tests.yml runs the suite on macOS for PRs touching the download/extract path. 53/53 assertions green locally; real production artifact (34MB/64MB) verified to pass through the new extraction path and run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… live termination [DEVA11Y-484] Addresses gaps found by stress-testing the guard rather than just asserting the happy path: - Measured overshoot: at a 200ms poll, bsdtar could write ~270-380MB past the cap on a fast disk before the watchdog tripped (the cap was far softer than the "200 MB" message implied). Tightened the poll to 50ms — a 10MB cap now peaks at ~34MB and a 2GB bomb is killed at ~224MB. Documented the cap as an explicit SOFT ceiling whose purpose is preventing disk *exhaustion*, not exact byte enforcement. - Windows Expand-Archive path was completely unguarded. Added a platform-agnostic post-extraction footprint backstop in the common path (typecheckable on macOS) so Windows rejects + cleans up a bomb before the binary is used. - Strengthened tests to assert the LIVE watchdog fires (bsdtar SIGTERM, status 15) and that peak disk stays bounded below the bomb size — previously the bomb tests would have passed even if only the post-extraction check worked (which would let a multi-GB bomb fill the disk). - Added test_large_bomb.sh (opt-in via DEVA11Y_DEEP=1): proves a 2GB bomb is bounded to ~224MB. Kept out of the default CI run to keep it fast/bounded. - README now documents the real limitations: soft cap + overshoot, Windows is post-hoc only, the Swift suite tests a mirror (not the compiled plugin) with the call sites typecheck-only, and locateExecutable's cap is defense-in-depth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
bsdtarwas invoked with no decompressed-size or entry-count limit, so an attacker who can influence the download URL (the HTTPS-only--download-url/BROWSERSTACK_A11Y_CLI_DOWNLOAD_URLoverride, or TLS interception of the default endpoint) could serve a decompression bomb that exhausts developer/CI disk space.scripts/bash/cli.sh; zsh/fish were byte-identical).What changed
Plugins/BrowserStackAccessibilityLint.swiftcurl --max-filesize(100 MB) caps the compressed download.bsdtaronce the decompressed footprint exceeds 200 MB or 10,000 entries. A cap on thecurl→bsdtarpipe would only bound compressed bytes — useless against a bomb — so the guard measures what lands on disk. The entry ceiling closes the "millions of tiny files" variant that stays small on disk.locateExecutablealso bounds enumeration at 10,000 entries (secondary defense).scripts/{bash,zsh,fish}/cli.shcurl --max-filesizecaps the compressed download.bsdtar … -Ooutput is piped throughhead -c(200 MB) withset -o pipefail; an oversized archive aborts (and deletes the partial binary) instead of filling the disk.head -cis a hard synchronous cap, so the shell path has no poll-gap and needs no entry check in-Omode (nothing is written per-entry).Tests (
scripts/test/, run viarun_tests.sh)Real
curl/bsdtar/headand the real Swift watchdog against crafted archives on a local server — no mocked unit tests.download_binaryis extracted verbatim from all three wrappers and run against the server; only the hardcoded URL is redirected (via a curl shim), so the security pipeline runs unmodified.check_drift.shfails CI if the mirror diverges from the plugin..github/workflows/extraction-guard-tests.ymlruns the suite on macOS for PRs touching the download/extract path.No breaking change to legitimate downloads
The real CLI artifact (macos arm64) is ~34 MB compressed / ~64 MB decompressed, so the 100 MB / 200 MB caps leave ~3× headroom. Verified end-to-end: the real production artifact passes through the new extraction path, extracts to a valid Mach-O, and runs.
Note on scope vs. the ticket
The ticket's primary vector (MitM on a plaintext download) is already closed by DEVA11Y-479 (override restricted to HTTPS; default URL is HTTPS). This PR is defense-in-depth against the remaining vectors: an attacker-controlled HTTPS override endpoint, or TLS interception.
Jira
DEVA11Y-484 · umbrella APPSEC-415
🤖 Generated with Claude Code