← Recipes

阅读中文版本

Audit npm Script Permissions for Supply-Chain Risk

List every package that runs lifecycle scripts on install, block preinstall and postinstall where untrusted, and record the allowlist in a checked-in policy file.

~25 minnpmsecuritysupply-chain

Task

Identify which npm dependencies run install-time scripts and decide which to allow before the next install.

When to use this

A transitive dependency adds a postinstall script that writes to a shared config file. CI breaks intermittently and nobody knows which package owns the change. The team wants install-time scripts visible and gated.

Prerequisites

  • Node.js 20 LTS and npm 10 or later
  • A lockfile committed to the repository
  • Read access to the package registry

Steps

1. List every package that declares a lifecycle script in the dependency tree.

npm ls --all --json | jq -r '.dependencies | paths(.scripts?) as $p | $p | join(" > ")'

Expected output: A tree of package names that have a scripts field, including preinstall, install, postinstall.

2. Inspect the specific scripts that run on install to judge whether they are safe.

npm view <pkg> scripts --json

Expected output: The exact command string for each lifecycle hook, so you can see if it touches the filesystem or network.

3. Configure npm to ignore lifecycle scripts by default and allow only the reviewed set.

// .npmrc
ignore-scripts=true
// package.json
"scripts": { "prepare": "node tools/allow-scripts.mjs" }

Expected output: A fresh npm install runs no postinstall scripts unless the allowlist explicitly permits them.

Troubleshooting

Problem

A needed native module fails to build

Solution

Add it to the allowlist and run its install script explicitly with npm rebuild <pkg> rather than re-enabling scripts globally.

Problem

The jq filter misses workspace packages

Solution

Use npm ls --all --workspaces --json and recurse into the workspace dependencies field as well.

Limitations

  • ignore-scripts disables all lifecycle hooks, so native modules must be rebuilt manually after install.
  • The allowlist is only as trustworthy as its reviewers; a reviewed package can still ship a malicious update later.

Make scripts visible

Most teams never read the scripts field of their dependencies. A postinstall that runs curl | bash is one transitive bump away from your CI. Start by listing every package that declares a lifecycle hook, then read the actual command before deciding.

npm ls --all --json | jq -r 'paths(.scripts?) as $p | $p | join(" > ")'

Gate by default

Turn scripts off globally in .npmrc and allow only the packages you have reviewed. A native module that needs to build goes in the allowlist; everything else stays silent. Re-run the audit whenever the lockfile changes.

Expected outcome

A fresh install runs no unapproved lifecycle scripts, the allowlist lives in version control, and CI fails fast if a new dependency tries to run a postinstall that is not yet reviewed.