Skip to content

SLICE-002 — Vulnerable read contrast: traversal ladder and broken sanitizer - #4

Merged
maximalfocus merged 2 commits into
mainfrom
issue/3-vulnerable-read
Aug 15, 2026
Merged

SLICE-002 — Vulnerable read contrast: traversal ladder and broken sanitizer#4
maximalfocus merged 2 commits into
mainfrom
issue/3-vulnerable-read

Conversation

@maximalfocus

Copy link
Copy Markdown
Owner

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).

  • Naive base-join retrieval (GET /documents) — joins the name to the tenant base and
    opens it, no resolution, no confinement. Reproduces cross-tenant disclosure, archive-root
    escape to the integration key + DEMO_SENTINEL, absolute-path override to the container's
    own /etc/passwd (CWE-36), and symlink escape (CWE-59). Surfaces the joined path via
    X-Boundless-Opened.
  • Broken "hardened" endpoint (GET /documents/hardened) — single-pass ../ strip on
    the raw, undecoded input, defeated by ....// (collapses back into ../) and by
    percent-encoded %2e%2e%2f (only decoded after the check).
  • Two-action opt-in — the vulnerable app requires the vulnerable Compose profile
    and ALLOW_VULNERABLE_DEMO=true; it refuses to boot without the acknowledgement.
    Default docker compose up never starts it.
  • Hardened, egress-blocked container — non-root, all caps dropped, 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).
  • Comparison engine + compare CLI, the read-axis regression matrix, and read-axis
    docs. Shared auth/lifespan/summary extracted into webcommon so the only intended
    difference 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 verify61 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.
  • Manual gate/hardening checks:
    • default docker compose up starts only secure;
    • the vulnerable service without ALLOW_VULNERABLE_DEMO exits with
      RuntimeError: Refusing to start … Set ALLOW_VULNERABLE_DEMO=true (exit 1);
    • egress from the vulnerable container to 1.1.1.1:53 times out (blocked).

Acceptance criteria → evidence

  1. Vulnerable app returns another tenant's statement / integration key + DEMO_SENTINEL /
    container /etc/passwd / out-of-root symlink content — tests/test_vulnerable_retrieval.py,
    compare rows.
  2. "Hardened" endpoint defeated by both ....// and %2e%2e%2f reaching the same targets —
    tests/test_broken_sanitizer.py, compare rows.
  3. Secure app indistinguishable generic 404 for every name, no base/absolute path revealed —
    SLICE-001 suite + compare (secure 404 on all rungs).
  4. Both apps identical output for benign retrieval and summary — tests/test_parity.py,
    compare parity rows.
  5. Every read-axis regression green; no path writes/deletes/mutates —
    tests/test_vulnerable_no_mutation.py, full suite.
  6. Ruff and mypy green through the same boundary locally and in CI — verify, ci.yml.
  7. Vulnerable not started by default; cannot start without both opt-in actions; non-root,
    caps dropped, no-new-privileges, read-only rootfs, no egress — manual checks above,
    docker-compose.yml, tests/test_vulnerable_gate.py.
  8. Docs wholly fictional, label the code local educational material, state the demo executes
    no command — README.md.

Risks / follow-ups

  • The write direction (Zip-Slip extraction, contained write proof, before/after tampered
    footer) is intentionally out of scope and lands in the next slice.
  • Starlette emits a StarletteDeprecationWarning about httpx under its TestClient; it does
    not 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

maximalfocus and others added 2 commits August 15, 2026 18:17
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>
@maximalfocus
maximalfocus merged commit 8693796 into main Aug 15, 2026
1 check passed
@maximalfocus
maximalfocus deleted the issue/3-vulnerable-read branch August 15, 2026 10:22
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.

SLICE-002 — Vulnerable read contrast: the traversal ladder and the broken sanitizer

1 participant