SLICE-003 — Vulnerable write contrast: Zip Slip and the contained write proof - #6
Merged
Merged
Conversation
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>
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 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.POST /documents/import— hand-rolled loop overzipfile, no per-entryconfinement. Importing an archive whose entries are
../../config/branding.confand../northwind-mills/statement-2026-07.txtwrites outside the caller's directory.later legitimate
GET /statements/summary(footer becomes the attacker's); thecross-tenant statement overwrite is visible when that tenant reads its own document.
400), writing no entry.compareCLI now shows the read ladder and the write before/after.Verification (all green through the same Compose boundary as CI)
docker compose run --rm verify→ 68 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:
before='Statements provided by the shared demo archive.'→after='PWNED-BY-ZIP-SLIP (demo)'via the legitimate summary;changed=True;400 (rejected whole), footer intact./etc/…and/usr/local/bin/…(execution-reaching) are refused; only the tmpfs fixture tree(
/data) is writable.Acceptance criteria → evidence
METHODOLOGY.md§0; containmentproven by
tests/test_contained_write.pyand the read-only-rootfs check.—
tests/test_vulnerable_write.py, compare write rows.tests/test_vulnerable_write.py, compare.400), no entry written, dirs unchanged —tests/test_contained_write.py::test_secure_rejects_the_same_archive_whole.truncation; verification asserts only the two targets change —
tests/test_contained_write.py::test_only_the_two_documented_targets_change, containercontainment check.
test_fresh_state_recreated_each_run; tmpfsrecreated on container start.
verify,ci.yml.target execution-reaching paths while this demo does not —
README.md.Risks / follow-ups
boundlesspublic withits license and security policy) is the separate
/idd-publishphase and is not part ofthis PR.
StarletteDeprecationWarningabouthttpxunder its TestClient; it doesnot 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
/tmpwas a second writable mount and an arbitrary archive member could reach it. Commit30b2cefremoves 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/dataas the sole writable mount;/tmp,/etc, and/usr/local/binrefuse writes; the HTTP/tmpprobe returns generic400. The full Compose gate is green at 68 tests, secure demo 16/16, and comparison 12/12.