The most effective single defense is to stop running dependency code at install time. pnpm 69,400 and Yarn Berry 11,798 refuse dependency build scripts by default (Build Script Approval), and npm 2,036 is following: npm 11.19 warns (Typosquatting), and npm 12 (8 July 2026, not yet bundled with Node.js 2,131 ) blocks them unless package.json allows them. Repeat the dependency-confusion install with npm 12.1.0, installed locally beside the system npm.
cd ~/v5-ch1/supply/consumer && rm -rf node_modules package-lock.json && npm pkg delete dependencies
N=~/v5-ch1/tools/npm12/node_modules/.bin/npm && $N --version
NPM_TOKEN=npm_fake_demo $N install booknest-internal-utils@^1.0.0 --foreground-scripts \
--no-audit --no-fund
$N install-scripts approve booknest-internal-utils
jq .allowScripts package.json12.1.0
added 1 package in 394ms
npm warn install-scripts 1 package had install scripts blocked because they are not covered by
allowScripts:
npm warn install-scripts booknest-internal-utils@1.99.0 (postinstall: node steal.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts
approve <pkg>` to allow.
Approved booknest-internal-utils:
added booknest-internal-utils@1.99.0
{
"booknest-internal-utils@1.99.0": true
}The postinstall never ran. Approval is pinned to the exact version (allow-scripts-pin), so a worm's next patch is blocked again until reviewed; strict-allow-scripts makes the warning fail CI. The wrong package was still installed, though, and runs the moment BookNest calls require(), so the scope rule of Typosquatting still matters. The second cheap defense is a release-age delay, which keeps a fresh version out of your installs until scanners and maintainers have had time to look at it. Yarn 4.18.1 applies one by default and refused the lab's package outright: All versions satisfying "^1.0.0" are quarantined, since both were minutes old.
| Manager | Install scripts | Release-age delay |
|---|---|---|
| npm 11 | Run, with a warning | min-release-age (days, off) |
| npm 12 | Blocked; allowScripts | min-release-age (days, off) |
| pnpm 11+ | Blocked; allowBuilds | minimumReleaseAge, 1 day |
| Yarn 4 | Blocked; enableScripts | npmMinimalAgeGate, 1 day |
| Bun 73,307 | Trusted list only | minimumReleaseAge in bunfig |
| pip 21,050 , uv 90,229 , Cargo 6,602 | Build code runs | uv --exclude-newer |
Other ecosystems have their own auditors (pip-audit, cargo audit, OWASP Dependency-Check, brew 6,457 vulns), and multi-ecosystem scanners such as Trivy 67,732 read every lockfile at once.
cd ~/v5-ch1/audit-demo # java, node, python, rust: this chapter's lockfiles, plus jinja2 3.1.2
trivy fs --quiet --scanners vuln --format json . | jq -r '.Results[]
| "\(.Target) (\(.Type)): \([.Vulnerabilities[]?.VulnerabilityID] | join(" "))"'java/gradle.lockfile (gradle): node/package-lock.json (npm): python/uv.lock (uv): CVE-2024-22195 CVE-2024-34064 CVE-2024-56201 CVE-2024-56326 CVE-2025-27516 rust/Cargo.lock (cargo):
BookNest, the Rust tool and the Java service are clean today; the Python project's deliberately old Jinja 15,439 pin has five known CVEs. Run such a scan on every change (GitHub adds Dependabot 29 alerts, Scanning with Trivy and Grype scans images), and read a clean result as "no known vulnerabilities": a worm published an hour ago is in no database yet, which is what release-age delays and blocked install scripts are for.