Install scripts are the supply-chain attack surface
The 2026 npm and PyPI compromises ran through lifecycle scripts and stolen publish tokens. A probe that shows what your install actually executes.
By the Maborak engineering team · · 4 min read
A dependency review that reads package names misses the part that runs. An npm package can execute a postinstall script with the developer’s or the CI runner’s privileges, and a PyPI package can do the same through a build hook. The 2026 compromises used that path and then reused whatever tokens the environment exposed to publish the next infected version.
Checked 29 October 2026. Incident names and dates below are from public advisories and vendor research; the mechanism is from the npm and PyPI documentation. Attribution is deliberately not asserted.
Background
The Shai-Hulud worm in 2025 established the pattern on npm: steal publish tokens from a compromised maintainer or CI environment, publish a new version with an install script, and repeat. In 2026 the reports continued across both ecosystems, including a May wave described as “Mini Shai-Hulud”, PyPI packages carrying a worm, and compromised releases of widely used libraries. The common mechanism, not any single package, is the lesson.
Mechanics
- Lifecycle scripts. npm runs
preinstall,installandpostinstallfrom a package’spackage.json. The script runs as the user installing, with access to the working directory, environment variables and, in CI, any secret the job exposes. - Build hooks. Python packaging can execute code during build and install.
pip installfrom a source distribution is the highest-risk path; a wheel with--require-hashesis more constrained. - Token reuse. CI environments commonly hold a registry token with publish scope. A script that reads it can publish to other packages the account owns. That is how a single package becomes a wave.
- Lockfile drift. Installing without a lockfile, or regenerating it during CI, fetches whatever is current. A lockfile pins versions and integrity hashes; it is the control that turns a moving target into a fixed one.
Impact
The blast radius is the CI runner and the developer workstation: source, environment variables, cloud credentials and registry tokens. The malicious version is usually published and removed within hours, so a clean audit later does not prove the install was clean.
Probe
Find which dependencies would run an install script, without installing them:
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
let n = 0;
for (const [k, v] of Object.entries(p.packages || {})) {
if (v.hasInstallScript) { n++; console.log(k); }
}
console.log('---', n, 'packages with install scripts');
JS
Then install with scripts disabled and run the tests that need a build step separately:
npm ci --ignore-scripts
npm rebuild <package> # only for the packages you reviewed and need
npm audit signatures
npm audit signatures verifies that installed packages carry valid registry signatures where the registry publishes them. It is evidence of provenance, not of intent.
On the site’s own lockfile the probe reports one package with an install script: fsevents, a macOS-only optional dependency. A count of one is the useful result; the point is to know which packages execute code, rather than assume none do. [reproduced]
Mitigation
- Install with
--ignore-scriptsby default, and rebuild only the packages that need native compilation, after reviewing them. - Scope registry tokens to the packages a job publishes, and keep publish tokens out of pull/build jobs entirely.
- Require lockfiles in CI and fail when they change without review. Fetch integrity hashes from the registry.
- Use PyPI hash-checking mode:
pip install --require-hashes -r requirements.txt. Prefer wheels to source distributions where you can. - Turn on registry provenance and two-factor authentication for maintainers.
- Limit egress from CI runners. A script can only exfiltrate to a host it can reach.
- Keep secret scanning on the repository and rotate anything that a compromised run could have read.
Verification
npm ci --ignore-scripts
npm test
npm audit signatures
git diff --exit-code package-lock.json
A test suite that passes after an install with scripts disabled proves the project does not need an unreviewed build hook. The lockfile diff proves CI installed what was committed.
Sources
- NHS England: supply chain attack affecting npm and PyPI packages (12 May 2026).
- Unit 42: the npm threat landscape (15 July 2026).
- GitGuardian: four more supply chain attacks (22 July 2026).
- npm: lifecycle scripts and PyPI hash-checking mode.