# Supply Chain Hardening

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

- Skill: `oimiragieo/supply-chain-hardening` (Agent Skill)
- Install (CLI): `npx skillmds@latest add oimiragieo/supply-chain-hardening`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oimiragieo/supply-chain-hardening/raw
- Safety review: WARNING
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Research & Search
- Author: oimiragieo (https://skillmd.com/u/oimiragieo)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oimiragieo/supply-chain-hardening

---


# 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.
```python
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)  # only after all members validated
```
- 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:
```powershell
$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
1. Bounded download helper (H2) returning a `.part`.
2. Checksum gate (H3) before any rename/extractall.
3. Path-traversal guard (H1) inside extract, before the first write.
4. Lock new deps (H4) before opening the PR.
5. 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.
6. CI: add `--locked`/`--frozen` to the new install step.
7. 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).

