SLICE-002 — Vulnerable read contrast: traversal ladder and broken sanitizer - #4
Merged
Conversation
Add the opt-in, hardened vulnerable application's read paths as a side-by-side contrast to the secure baseline (SLICE-002): - naive base-join retrieval (no resolution, no confinement) reproducing cross-tenant disclosure, archive-root escape to the integration key and its DEMO_SENTINEL, absolute-path override to the container's own /etc/passwd, and symlink escape out of the archive root; - a "hardened" endpoint whose single-pass ../ strip on the raw undecoded input is defeated by ....// (collapses back into ../) and by percent-encoded %2e%2e%2f (only decoded after the check); - a hardened, egress-blocked container (non-root, all caps dropped, no-new-privileges, read-only rootfs) gated behind a Compose profile AND an explicit ALLOW_VULNERABLE_DEMO=true acknowledgement; - a vulnerable-vs-secure comparison engine and `compare` CLI, the read-axis regression matrix, and read-axis documentation. Shared auth/lifespan/summary extracted into webcommon so the only intended difference between the two apps is how each locates a file. No write/Zip-Slip behaviour is introduced. Refs #3 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The secure demo performs a well-formed import that mutates the long-running secure container's fixtures. The comparison needs both apps to start from identical fresh state, so tear down between the two steps. Refs #3 Co-Authored-By: Claude Opus 5 (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
Adds the deliberately vulnerable application's read paths as a side-by-side
contrast to the secure baseline (SLICE-002). The vulnerable app is opt-in and hardened;
the secure app stays the default and keeps refusing every rung. No write / Zip-Slip
behaviour is introduced (that is a later slice).
GET /documents) — joins the name to the tenant base andopens it, no resolution, no confinement. Reproduces cross-tenant disclosure, archive-root
escape to the integration key +
DEMO_SENTINEL, absolute-path override to the container'sown
/etc/passwd(CWE-36), and symlink escape (CWE-59). Surfaces the joined path viaX-Boundless-Opened.GET /documents/hardened) — single-pass../strip onthe raw, undecoded input, defeated by
....//(collapses back into../) and bypercent-encoded
%2e%2e%2f(only decoded after the check).vulnerableCompose profileand
ALLOW_VULNERABLE_DEMO=true; it refuses to boot without the acknowledgement.Default
docker compose upnever starts it.no-new-privileges,read-only root filesystem, tmpfs fixture tree, and no network egress (attached to a
bridge with IP masquerade disabled; loopback publish still works).
compareCLI, the read-axis regression matrix, and read-axisdocs. Shared auth/lifespan/summary extracted into
webcommonso the only intendeddifference between the two apps is how each locates a file (guaranteeing parity).
Verification (all green through the same Compose boundary as CI)
docker compose run --rm verify→ 61 passed, Ruff + mypy (strict) clean.docker compose run --rm demo→ secure baseline 16/16 (regression after the refactor).ALLOW_VULNERABLE_DEMO=true docker compose --profile vulnerable run --rm compare→8/8 rows: every traversal rung shows the vulnerable app crossing (200, with the
un-normalized joined path surfaced) while the secure app refuses (404); legitimate parity
holds.
docker compose upstarts onlysecure;ALLOW_VULNERABLE_DEMOexits withRuntimeError: Refusing to start … Set ALLOW_VULNERABLE_DEMO=true(exit 1);1.1.1.1:53times out (blocked).Acceptance criteria → evidence
DEMO_SENTINEL/container
/etc/passwd/ out-of-root symlink content —tests/test_vulnerable_retrieval.py,compare rows.
....//and%2e%2e%2freaching the same targets —tests/test_broken_sanitizer.py, compare rows.404for every name, no base/absolute path revealed —SLICE-001 suite + compare (secure
404on all rungs).tests/test_parity.py,compare parity rows.
tests/test_vulnerable_no_mutation.py, full suite.verify,ci.yml.caps dropped,
no-new-privileges, read-only rootfs, no egress — manual checks above,docker-compose.yml,tests/test_vulnerable_gate.py.no command —
README.md.Risks / follow-ups
footer) is intentionally out of scope and lands in the next slice.
StarletteDeprecationWarningabouthttpxunder its TestClient; it doesnot affect results.
Safety
Wholly synthetic and local; executes no command. The vulnerable app discloses files but
writes/deletes/mutates nothing, is opt-in twice over, and runs in a hardened, egress-blocked
container. Do not deploy it.
Closes #3