| name | supply-chain-hardening |
| description | Use BEFORE writing any code that extracts an archive, downloads a binary/wheel/tarball, installs a dependency or tool, self-upgrades a running binary, or bootstraps a toolchain (rustup/uv/mise). Triggers on: 'extract a zip/tar', 'extractall', 'download the release asset', 'self-update/auto-upgrade helper', 'install script', 'pin the dependency', 'cargo/npm/pip install in CI', 'bootstrap the toolchain'. Encodes 6 production-shipped, citation-backed hardening checks: zip-slip guard before extractall; time-bound + byte-capped downloads; checksum-gated FAIL-CLOSED installs (incl. detached self-upgrade helpers, verify before os.replace); --locked + exact version pins for CI tools; fail-closed-with-explicit-opt-in for unverified toolchains; committed-checksum for platform installers whose native bootstrap skips binary verification (uv .ps1/.sh). Apply BEFORE writing the code, not after. |
supply-chain-hardening
A reusable checklist + cited principles for implementing archive extraction, binary downloads, self-upgrade helpers, dependency installation, or toolchain bootstrapping. All six patterns shipped to production in tensor-grep v1.17.2–v1.17.18. Apply BEFORE writing the code.
When to invoke
Trigger when the task involves ANY of:
- Extracting a ZIP/TAR/RAR/7z whose path comes from untrusted input (network response, user URL, release-asset filename).
- Downloading a binary, wheel, tarball, or installer over the network.
- Installing a tool or dependency set (pip/uv, cargo, npm, go, apt, brew, winget).
- A self-upgrade or deferred/detached helper that replaces a running binary.
- Bootstrapping a toolchain (rustup, uv, volta, mise) — the toolchain itself is a supply input.
- A CI step that pulls external artifacts.
Real receipts (tensor-grep)
- zip-slip guard on the benchmark
rg.zip extractall; npm install.js download timeout + 256 MiB byte cap; a test-side zip-slip guard reusing the production _safe_extract_zip.
- native front-door download timeouts (
urlopen(timeout=30) + socket timeout bounding urlretrieve) — closed an indefinite install/upgrade hang.
cargo-audit==0.22.2 --locked + cargo-deny --locked — an unpinned cargo install had pulled a breaking upstream release that reddened the CI security gate.
- BOTH detached Windows self-upgrade helpers now fail-closed verify sha256 — the parent injects the expected SHA from
CHECKSUMS.txt, the helper re-verifies BEFORE os.replace. Adversarially validated by compile()+exec() of the generated helper string, asserting the gate fires before replace (see dogfood-the-shipped-artifact).
The five checklist items
H1 — Zip-slip / path-traversal guard before extractall
Archive entries carry attacker-controlled paths; ZipFile.extractall / TarFile.extractall do NOT block ../ or symlinks by default.
import pathlib, zipfile
def safe_extract(archive: pathlib.Path, dest: pathlib.Path) -> None:
dest = dest.resolve()
with zipfile.ZipFile(archive) as zf:
for m in zf.infolist():
target = (dest / m.filename).resolve()
if not str(target).startswith(str(dest) + "/"):
raise ValueError(f"Zip-slip blocked: {m.filename!r}")
zf.extractall(dest)
- Canonicalize the joined path BEFORE the startswith check. The trailing separator on
dest + "/" is load-bearing (/safe must not match /safe-evil).
- TAR: reject/validate symlink + hardlink entries against dest too.
- Enforce per-entry AND total-bytes caps (zip-bomb vector). Never
startswith("..") — bypassable via encoding/absolute/UNC paths.
H2 — Time-bound + byte-capped downloads
An unbounded download hangs the agent or fills disk — both DoS, not annoyances.
- Apply the size cap IN-LOOP per chunk (do not buffer the whole body first).
- Separate connect timeout from read timeout.
- Enforce a minimum plausible size for binaries (a 200 + tiny error page must fail-closed).
- Download to
.part/.tmp; rename to final only after verification.
- Cap manifests separately (~1 MB) — a large version manifest is anomalous.
H3 — Checksum-gated FAIL-CLOSED installs (incl. deferred self-upgrade helpers)
Verify BEFORE extraction/replacement, never after. Canonical order:
download -> verify SHA256 -> [deferred helper: RE-VERIFY in the helper] -> atomic rename/replace -> extract
NOT download -> extract -> verify (extraction already ran attacker code).
- For detached helpers (spawned to replace the parent after it exits), re-verify SHA256 immediately before the rename — this closes the TOCTOU window between parent-download and helper-replace.
- Atomic rename old->
.old, new->target; on failure roll back .old->target and delete .part.
- Fail-closed = absence of the checksum sidecar is a HARD ABORT, not a warning, unless
--allow-unverified is explicit.
- SHA256 is the minimum; pair with Ed25519/minisign when you control the signing key.
H4 — Pinned + --locked dependency/tool installs
Loose ranges (^, ~>, >=) let a freshly-published malicious version in silently. Need BOTH exact pins in the manifest AND --frozen/--locked enforced in CI.
| Ecosystem | Manifest pin | CI install |
|---|
| uv | ==1.2.3 | uv sync --locked |
| pip | ==1.2.3 | pip install -r req.txt --require-hashes |
| cargo | = "1.2.3" | cargo install --locked |
| npm | exact | npm ci |
| pnpm | exact | pnpm install --frozen-lockfile |
| go | go.sum present | go mod verify && go build |
Also: minimum-release-age cooldown (pnpm/Yarn Berry/Bun/uv), allowlist postinstall scripts (onlyBuiltDependencies), run npm/cargo/pip audit on every CI run. | | |
H5 — Fail-closed with explicit opt-in for unverified toolchains
Bootstrapping rustup/uv/mise/volta/conda is a full supply-chain trust decision. Default: refuse without a known-good checksum or SLSA provenance; require an explicit, loudly-warned --allow-unverified opt-in.
- Pin the toolchain version AND its sha256 in config (
.tool-versions, rust-toolchain.toml, [tool.uv]), not in ad-hoc scripts.
- SLSA L2+: verify the provenance signature, not just the content hash (hash alone trusts the download host).
H6 — Platform-asymmetric installer trust: uv .ps1 has NO binary checksum (uv issue #13074)
uv's .ps1 Windows installer does NOT verify the downloaded binary's checksum; the .sh self-verifies (uv ≥0.11.0). Do NOT invoke the .ps1 directly from CI or install scripts — it trusts whatever the CDN returns.
Fix: commit a dual-arch SHA-256 table (e.g. scripts/uv_checksums.json with x86_64-pc-windows-msvc and aarch64-pc-windows-msvc keys) pinned to the exact release. Download the release binary separately, verify FAIL-CLOSED before use:
$actual = (Get-FileHash -Algorithm SHA256 -Path $uvExe).Hash.ToLower()
$expected = $checksums[$arch] # from committed table
if ($actual -ne $expected) { throw "uv checksum mismatch: $actual vs $expected" }
- Linux/macOS keep the self-verifying pinned
.sh — no change needed there.
- SHA-confirmation discipline: ALWAYS download+hash to CONFIRM a committed SHA. Never trust an agent-reported or sidecar value (an agent saying "I fetched the SHA from the release page" is not a verification — you must
Get-FileHash the binary yourself against the value in source control). This is the discipline that confirmed the x86_64 SHA matched the committed value byte-for-byte in a prior tensor-grep hardening batch.
Antipatterns -> correct substitute
| Antipattern | Correct |
|---|
extractall(dest) with no guard | normalize + startswith check every member first |
| verify checksum AFTER extraction | verify BEFORE; delete artifact on mismatch |
| download with no timeout/size cap | bounded streaming download, per-chunk accumulator |
^1.0 / >=2.0 ranges in CI | exact pins + --locked/--frozen/npm ci |
deferred updater trusts parent's .part | re-verify SHA256 in the helper before rename |
| warn on missing checksum sidecar | hard abort; require --allow-unverified |
startswith("..") filter | resolve canonical path + startswith(dest + "/") |
os.rename with no rollback | old->.old, new->target; restore on failure |
| postinstall from all npm packages | allowlist via onlyBuiltDependencies |
| SHA256 alone for self-upgrade | SHA256 + signature against an offline key |
uv .ps1 bootstrap with no checksum (uv issue #13074) | committed dual-arch SHA-256 table + Get-FileHash FAIL-CLOSED (H6); never trust an agent-reported or sidecar SHA |
Implementation order for a new feature
- Bounded download helper (H2) returning a
.part.
- Checksum gate (H3) before any rename/extractall.
- Path-traversal guard (H1) inside extract, before the first write.
- Lock new deps (H4) before opening the PR.
- If bootstrapping a toolchain, add the opt-in gate (H5) as the outermost guard. On Windows (e.g. uv), skip the platform
.ps1 and apply H6 (committed dual-arch SHA-256 table + Get-FileHash FAIL-CLOSED) — the .ps1 has no binary checksum.
- CI: add
--locked/--frozen to the new install step.
- Review: confirm order is download -> verify -> rename/extract, never download -> extract -> verify. For generated/detached helpers, EXECUTE the generated code to prove the gate fires (see
dogfood-the-shipped-artifact).
Grounding
OWASP CWE-22/23 (zip-slip) · Android "Zip Path Traversal" (2024-09) · CodeQL java-zipslip · .NET zip/tar best-practices · SLSA v1.2 "Verifying artifacts" · Trail of Bits SLSA (2024-10) · NIST SP 800-218 (SSDF) PS.3.2/PW.4.4/SR-4 · NIST SP 800-204D · jordanconway/package-manager-hardening · mise PR #8593 · minio/selfupdate apply.go · rs-selfupdater · 1995parham-learning/auto-update-binary.
History
- 2026-06-28: created from a tensor-grep v1.17.x supply-chain hardening batch.
- 2026-06-29: H6 added — uv
.ps1 platform-asymmetric installer gap (uv issue #13074) + SHA-confirmation discipline; antipattern row + implementation order updated (a tensor-grep hardening batch).
- 2026-07-01: checksum-gates the Unix
.sh uv installer too (v1.17.18) — dropped curl|sh remote-script exec for a fetched GitHub archive + fail-closed SHA verify, mirroring the Windows H6 path. Cross-platform parity: both scripts/install.ps1 and scripts/install.sh now verify a committed checksum before use (frontmatter/intro count 5 → 6).