Skip to content

SLICE-003 — Vulnerable write contrast: Zip Slip and the contained write proof - #6

Merged
maximalfocus merged 2 commits into
mainfrom
issue/5-vulnerable-write
Aug 15, 2026
Merged

SLICE-003 — Vulnerable write contrast: Zip Slip and the contained write proof#6
maximalfocus merged 2 commits into
mainfrom
issue/5-vulnerable-write

Conversation

@maximalfocus

@maximalfocus maximalfocus commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the write direction of the vulnerability (SLICE-003), completing the read/write
contrast. The vulnerable import is a hand-rolled per-entry write loop that joins each entry
name to the destination and writes it with no confinement (Zip Slip); the secure app keeps
rejecting the identical archive as a whole. Every write is confined to two documented
targets inside the demo's own disposable in-container fixture tree, in conformance with the
security-demos/METHODOLOGY.md §0 contained-write carve-out.

  • Vulnerable POST /documents/import — hand-rolled loop over zipfile, no per-entry
    confinement. Importing an archive whose entries are ../../config/branding.conf and
    ../northwind-mills/statement-2026-07.txt writes outside the caller's directory.
  • Observable through the normal boundary — the branding-config overwrite returns via a
    later legitimate GET /statements/summary (footer becomes the attacker's); the
    cross-tenant statement overwrite is visible when that tenant reads its own document.
  • Secure app rejects the identical archive whole (400), writing no entry.
  • compare CLI now shows the read ladder and the write before/after.

Verification (all green through the same Compose boundary as CI)

  • docker compose run --rm verify68 passed, Ruff + mypy (strict) clean.
  • docker compose run --rm demo → secure baseline 16/16.
  • ALLOW_VULNERABLE_DEMO=true docker compose --profile vulnerable run --rm compare
    12/12 rows (6 read + 2 parity + 4 write). The write rows show:
    • footer before='Statements provided by the shared demo archive.'
      after='PWNED-BY-ZIP-SLIP (demo)' via the legitimate summary;
    • cross-tenant document changed=True;
    • secure import of the same archive → 400 (rejected whole), footer intact.
  • Container containment — with a read-only root filesystem, writes to /etc/… and
    /usr/local/bin/… (execution-reaching) are refused; only the tmpfs fixture tree
    (/data) is writable.

Acceptance criteria → evidence

  1. Carve-out in place and conformed to — verified against METHODOLOGY.md §0; containment
    proven by tests/test_contained_write.py and the read-only-rootfs check.
  2. Zip-Slip write escapes the extraction dir; legitimate summary returns the attacker footer
    tests/test_vulnerable_write.py, compare write rows.
  3. Second entry overwrites another tenant's document; CLI before/after —
    tests/test_vulnerable_write.py, compare.
  4. Secure rejects the identical archive whole (400), no entry written, dirs unchanged —
    tests/test_contained_write.py::test_secure_rejects_the_same_archive_whole.
  5. No write outside the disposable tree, no execution-reaching path, no delete / out-of-target
    truncation; verification asserts only the two targets change —
    tests/test_contained_write.py::test_only_the_two_documented_targets_change, container
    containment check.
  6. Fixture tree recreated fresh every run — test_fresh_state_recreated_each_run; tmpfs
    recreated on container start.
  7. Write-axis regressions green; Ruff + mypy green locally and in CI — verify, ci.yml.
  8. Docs state writes confined to two documented targets and that the same primitive could
    target execution-reaching paths while this demo does not — README.md.

Risks / follow-ups

  • This completes the three implementation slices. Publication (making boundless public with
    its license and security policy) is the separate /idd-publish phase and is not part of
    this PR.
  • Starlette emits a StarletteDeprecationWarning about httpx under its TestClient; it does
    not affect results.

Safety

Wholly synthetic and local; executes no command. Every write is confined to two documented,
non-execution-reaching targets inside a disposable in-container fixture tree recreated each
run; the hardened, egress-blocked, read-only-rootfs container refuses any write outside that
tree. Do not deploy it.

Closes #5

Containment repair

A direct runtime audit found that /tmp was a second writable mount and an arbitrary archive member could reach it. Commit 30b2cef removes that mount and adds an all-members preflight safety rail: ordinary imports may remain inside the caller tenant directory, the two documented fixture escapes remain demonstrable, and every other resolved target is rejected before any write. Regression coverage proves a mixed archive cannot partially alter the footer or create an out-of-fixture file. Runtime readback now shows /data as the sole writable mount; /tmp, /etc, and /usr/local/bin refuse writes; the HTTP /tmp probe returns generic 400. The full Compose gate is green at 68 tests, secure demo 16/16, and comparison 12/12.

maximalfocus and others added 2 commits August 15, 2026 18:33
Add the write direction of the vulnerability (SLICE-003):

- a vulnerable POST /documents/import that extracts with a hand-rolled per-entry
  write loop joining each entry name to the destination with no confinement, so a
  ../-bearing entry escapes the extraction directory (Zip Slip);
- the escape overwrites the branding config the app reads back (the tamper returns
  through a later legitimate GET /statements/summary) and another tenant's statement
  document (visible via that tenant's own read);
- the secure app rejects the identical archive as a whole with a generic 400,
  writing no entry;
- the compare CLI shows the read ladder AND the write before/after through the
  normal boundary; write-axis regressions and docs.

Every write is confined to two documented targets inside the disposable in-container
fixture tree, recreated on every run; no delete, no truncation outside those targets,
no execution-reaching path, no host access — verification asserts everything else is
byte-for-byte unchanged, and the hardened container's read-only root filesystem
refuses any write outside the tmpfs tree. Conforms to the METHODOLOGY §0 contained-
write carve-out.

Refs #5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maximalfocus
maximalfocus merged commit fc30dd1 into main Aug 15, 2026
1 check passed
@maximalfocus
maximalfocus deleted the issue/5-vulnerable-write branch August 15, 2026 11:38
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-003 — Vulnerable write contrast: Zip Slip and the contained write proof

1 participant