A routine dependency update ships malicious code
“A new patch release of a package, often a transitive one, steals tokens during install or injects code into the bundle. It appears in the lockfile hours after publication and CI picked it up automatically.”
THE PRINCIPLE
Every install is code execution with your credentials. Make new code arrive deliberately: pinned, aged, and with scripts off by default.
FIRST MOVES
- If hit: pin the last good version, then rotate every secret reachable from the machines and CI jobs that installed it (npm, GitHub, cloud, .env).
- Use frozen installs in CI (pnpm install --frozen-lockfile, npm ci) and review lockfile diffs like code.
- Add a release-age cooldown: pnpm minimumReleaseAge (minutes; pnpm 10.16+, default 1440 since pnpm 11) or npm min-release-age (days; npm 11.10+, off by default).
- Allow install scripts only for reviewed packages: pnpm allowBuilds (10.26+; onlyBuiltDependencies before that), or npm allowScripts in package.json (enforced from npm 12; on npm 11 it only warns, so set ignore-scripts=true). Keep git and tarball sources out of transitive deps.
- For packages you publish: npm trusted publishing via OIDC, provenance, and 'disallow tokens' in package settings.
TOOLS: THEN → NOW
Caret ranges resolved fresh on every installCommitted lockfiles, frozen installs, and a minimum release age
Every dependency runs install scriptsPer-package script allowlists (pnpm allowBuilds, npm 12 allowScripts) with unreviewed scripts blocked
Long-lived npm automation tokens in CITrusted publishing (OIDC) with automatic provenance
PATTERN SNAPSHOT
# pnpm-workspace.yaml (version that added each setting in comments)
minimumReleaseAge: 4320 # 10.16+, minutes: skip versions younger than 3 days
minimumReleaseAgeExclude:
- "@myorg/*" # your own packages ship on your schedule
trustPolicy: no-downgrade # 10.21+: fail if a release has weaker trust evidence than earlier ones
blockExoticSubdeps: true # 10.26+, default since 11: only direct deps may use git/tarball sources
allowBuilds: # 10.26+: only listed packages may run install scripts
esbuild: true
core-js: false
# npm equivalent in .npmrc
# min-release-age=3 # days, npm 11.10+
# ignore-scripts=true # npm 11 only; npm 12 blocks scripts missing from package.json "allowScripts"CLOSE THE AI. EXPLAIN THIS.
A compromised package ran a postinstall script on a CI runner for ten minutes. Why is downgrading the package not enough, and what must you rotate?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
- pnpm: Dependency resolution settings (minimumReleaseAge, trustPolicy) (opens in new tab)Checked
- pnpm: Build settings (allowBuilds, strictDepBuilds) (opens in new tab)Checked
- pnpm 10.16 release notes (minimumReleaseAge and why it helps) (opens in new tab)Checked
- pnpm 10.26 release notes (allowBuilds, blockExoticSubdeps) (opens in new tab)Checked
- npm Docs: config (min-release-age, ignore-scripts, allow-git) (opens in new tab)Checked
- npm Docs: npm approve-scripts (allowScripts, blocked by default in npm 12) (opens in new tab)Checked
- npm Docs: Trusted publishing for npm packages (opens in new tab)Checked
Guide reviewed