When you run npm install, you are not just downloading code. You are executing it. npm lifecycle scripts, particularly preinstall, install, and postinstall, run arbitrary code on your machine during package installation, with your user privileges, your file system access, and your network access.
This is by design. And it is one of the most exploited attack vectors in the JavaScript supply chain.
How Lifecycle Scripts Work
npm supports several lifecycle scripts that execute automatically at different stages of the package management process:
- preinstall: Runs before the package is installed
- install: Runs during installation
- postinstall: Runs after the package is installed
- preuninstall/uninstall/postuninstall: Run during package removal
- prepublish/prepare/prepublishOnly: Run before package publication
These scripts are defined in a package's package.json under the scripts field. When you install a package with npm, its lifecycle scripts execute automatically. This includes scripts in every dependency and transitive dependency in the tree.
A typical Node.js project with 500-1,000 dependencies may execute dozens of lifecycle scripts during a fresh npm install. Each script has the same privileges as the user running the installation.
The Attack Surface
Lifecycle scripts are attractive to attackers because:
Automatic execution. No user interaction is required. The scripts run as part of the normal installation workflow that every developer performs routinely.
Invisible by default. npm does not prominently display which packages have lifecycle scripts or what those scripts do. The output scrolls by in the terminal, often unnoticed.
Full system access. Lifecycle scripts can read and write files, make network connections, access environment variables (including credentials), install additional software, and modify the system configuration.
Transitive reach. You do not need to directly install a malicious package. If any package in your dependency tree depends on a malicious package with lifecycle scripts, those scripts execute when you install the top-level package.
CI/CD amplification. Lifecycle scripts execute in CI/CD environments, where they often have access to deployment credentials, artifact registries, and production infrastructure.
Real-World Exploitation
Lifecycle scripts have been central to numerous supply chain attacks:
Credential harvesting. Malicious postinstall scripts that read .npmrc files (which may contain authentication tokens), environment variables, SSH keys, and other credentials, then exfiltrate them to attacker-controlled servers.
Cryptominers. Packages with install scripts that download and execute cryptocurrency mining software on the developer's machine and in CI/CD workers.
Reverse shells. Postinstall scripts that establish reverse shell connections, giving attackers persistent access to development and build environments.
Dependency confusion. Internal package names claimed on the public npm registry, with malicious postinstall scripts that execute when organizations' build systems resolve the public package instead of the intended internal one.
Typosquatting. Packages with names similar to popular packages (e.g., lodahs instead of lodash) that contain malicious lifecycle scripts to exploit installation typos.
The North Korean-linked Lazarus Group has repeatedly used npm lifecycle scripts as an attack vector, publishing packages with postinstall scripts that deploy information-stealing malware to developer machines.
Why This Persists
Lifecycle scripts exist for legitimate purposes:
- Native addon compilation. Packages with C/C++ native addons (like
node-sass,bcrypt,sharp) use install scripts to compile platform-specific binaries. - Post-install setup. Some packages need to download platform-specific binaries or configure environment-specific settings.
- Development tooling. Tools that need to set up git hooks, configure environments, or install companion utilities.
Removing lifecycle scripts entirely would break a significant portion of the npm ecosystem. The challenge is allowing legitimate use cases while preventing malicious ones.
Mitigations
Several approaches reduce lifecycle script risk:
Disable Scripts by Default
The most effective mitigation is running npm install --ignore-scripts and then manually running scripts for packages that require them. This inverts the trust model: scripts are blocked by default and allowed by exception.
In .npmrc:
ignore-scripts=true
The challenge is identifying which packages genuinely need lifecycle scripts. Native addons like sharp or bcrypt will not function without their install scripts. This approach requires maintaining an allowlist of packages permitted to run scripts.
Use a Lockfile and Audit
Lock files (package-lock.json) ensure that the same dependency versions are installed consistently. Combined with npm audit, this provides a baseline for detecting known malicious packages.
However, lock files do not prevent lifecycle script execution. They prevent dependency version drift, which is a related but distinct concern.
Socket and Package Behavior Analysis
Tools like Socket analyze package behavior rather than just known vulnerabilities. They flag packages that:
- Access the network during installation
- Read sensitive files (
.npmrc,.env, SSH keys) - Execute shell commands
- Obfuscate code in lifecycle scripts
This behavioral analysis catches novel attacks that vulnerability databases cannot.
Corepack and Managed Package Managers
Corepack, included with Node.js, provides a managed way to use specific package manager versions, reducing the risk of tampered package manager binaries.
npm Package Provenance
npm's package provenance feature, which links published packages to their source repositories and build processes through Sigstore attestations, helps verify that packages come from their claimed source. Lifecycle scripts in provenance-verified packages can at least be traced to their source code.
Sandboxed Installation
Running npm install in a sandboxed environment (Docker container, VM, or sandboxed process) limits the damage a malicious lifecycle script can cause. The script may execute, but it cannot access the host file system, network, or credentials outside the sandbox.
Organizational Best Practices
For development teams:
- Audit lifecycle scripts in direct dependencies. Review the install scripts of packages you directly depend on.
npm show <package> scriptsreveals the defined scripts. - Monitor for new lifecycle scripts. When updating dependencies, check whether new install scripts have been added. This can indicate compromise.
- Use
--ignore-scriptsin CI/CD. Build environments should not execute arbitrary install scripts. Run necessary compilation steps explicitly rather than through lifecycle hooks. - Minimize dependencies. Fewer dependencies mean fewer lifecycle scripts. Evaluate whether each dependency justifies the trust you are placing in it.
- Pin dependency versions. Use exact versions in
package.jsonand commit lock files. This prevents automatic adoption of compromised updates. - Separate build and runtime dependencies. Ensure
devDependenciesare not installed in production builds, reducing the lifecycle script surface in deployed artifacts.
The Ecosystem Trajectory
npm and the Node.js community are slowly moving toward safer defaults:
- npm's RFC for restricting lifecycle scripts to explicitly allowed packages is under discussion
- Package provenance adoption is increasing, providing transparency about package origins
- Behavioral analysis tools are maturing and integrating into CI/CD pipelines
- Alternative package managers like Bun are exploring different trust models for script execution
The fundamental tension between convenience (scripts just work) and security (scripts just run arbitrary code) will not be resolved quickly. Until the ecosystem shifts, organizations need to actively manage lifecycle script risk.
How Safeguard.sh Helps
Safeguard.sh provides visibility into the composition of your npm dependency tree, including identifying packages with lifecycle scripts that represent potential execution risks.
Through SBOM generation and continuous monitoring, Safeguard.sh tracks changes in your dependency graph, alerting when new packages with install scripts are added or when existing dependencies add lifecycle scripts in updates. This change detection is critical for catching supply chain compromises that operate through lifecycle script injection.
Combined with vulnerability monitoring and policy enforcement, Safeguard.sh helps organizations manage the npm lifecycle script risk as part of a comprehensive supply chain security strategy.