Skip to content

fix(security): cap bsdtar extraction size to prevent decompression bomb DoS [DEVA11Y-484]#25

Open
maunilm wants to merge 3 commits into
mainfrom
fix/DEVA11Y-484-bsdtar-size-limit
Open

fix(security): cap bsdtar extraction size to prevent decompression bomb DoS [DEVA11Y-484]#25
maunilm wants to merge 3 commits into
mainfrom
fix/DEVA11Y-484-bsdtar-size-limit

Conversation

@maunilm
Copy link
Copy Markdown
Collaborator

@maunilm maunilm commented May 29, 2026

Summary

  • CWE-400 (Uncontrolled Resource Consumption) — OWASP A05. bsdtar was 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_URL override, or TLS interception of the default endpoint) could serve a decompression bomb that exhausts developer/CI disk space.
  • Fix covers all four affected surfaces: the Swift SPM plugin and the bash, zsh, and fish wrappers (the ticket only flagged scripts/bash/cli.sh; zsh/fish were byte-identical).
  • Ships with a real-process regression suite (no mocks) — see below.

What changed

Plugins/BrowserStackAccessibilityLint.swift

  • curl --max-filesize (100 MB) caps the compressed download.
  • A background watchdog terminates bsdtar once the decompressed footprint exceeds 200 MB or 10,000 entries. A cap on the curl→bsdtar pipe 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.
  • A post-extraction footprint check makes detection deterministic on fast disks: a bomb that finishes decompressing within a single 200 ms poll interval is caught and cleaned up rather than slipping past the live watchdog.
  • Applied to both the remote and local extraction paths; on rejection the partial directory is removed and the run aborts.
  • locateExecutable also bounds enumeration at 10,000 entries (secondary defense).

scripts/{bash,zsh,fish}/cli.sh

  • curl --max-filesize caps the compressed download.
  • bsdtar … -O output is piped through head -c (200 MB) with set -o pipefail; an oversized archive aborts (and deletes the partial binary) instead of filling the disk. head -c is a hard synchronous cap, so the shell path has no poll-gap and needs no entry check in -O mode (nothing is written per-entry).

Tests (scripts/test/, run via run_tests.sh)

Real curl/bsdtar/head and the real Swift watchdog against crafted archives on a local server — no mocked unit tests.

  • Shell: the REAL download_binary is 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.
  • Swift: SwiftPM command plugins can't be imported by a test target, so a mirror harness compiles the guard block verbatim and drives real processes; check_drift.sh fails CI if the mirror diverges from the plugin.
  • Scenarios: legit (downloads → extracts → runs), 400 MB decompression bomb, 20k-entry bomb, oversized (>100 MB) download, corrupt archive, multi-file archive, missing URL/network failure.
  • Fixtures are bounded (≤400 MB, gitignored) and bomb tests use a small cap, so a regressed guard can never exhaust the disk. 53/53 assertions green locally; full run ~9 s; disk usage flat before/after.
  • CI: .github/workflows/extraction-guard-tests.yml runs 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

…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>
@maunilm maunilm requested a review from a team as a code owner May 29, 2026 12:28
maunilm and others added 2 commits June 2, 2026 16:44
…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>
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