Back to Blog

My Checklist for Every New npm Supply-Chain Attack

#security#npm#supply-chain
My Checklist for Every New npm Supply-Chain Attack

Feels like every couple of months there’s a fresh headline: some popular npm package got compromised, a maintainer got phished, or a worm crawled through a postinstall script and quietly stole tokens off people’s CI machines. I used to just skim it, think “glad that’s not one of mine,” and get back to work. Now, with a few client projects running alongside this site, that shrug doesn’t really hold up anymore. So there’s a routine I actually run every time a new one shows up — not just read about and forget three days later.

Figure out what’s actually affected first

Headlines always make it sound apocalyptic — “biggest npm supply-chain attack ever” or whatever — but once you actually open the advisory, the details are usually narrow. One package, one version range, sometimes just one maintainer whose account got taken over via phishing. I don’t trust a newsletter summary or an X thread for this. Straight to the GitHub Security Advisory or the official audit entry, note the package and the affected range, then figure out if it’s even remotely related to my stack.

Half the time it isn’t. Half the “incidents” blowing up the timeline never touch me at all.

Diff the lockfile, don’t just reinstall

If there’s a match, I don’t just take npm audit’s word for it. I run npm ls <package> in every repo to see if it’s a direct dependency or something pulled in transitively, then actually open package-lock.json to check the exact pinned version. If the pinned version falls outside the affected range, I note it for a future upgrade — I don’t treat it as an emergency.

Rotate CI tokens first, not last

This is the step that’s easiest to skip when you’re in a hurry, and it matters the most. If there’s any realistic chance a dependency I use got compromised, every secret used in that pipeline — npm publish tokens, GitHub Actions secrets, VPS deploy keys — gets treated as potentially leaked, and rotated before I do anything else. It’s cheap, a couple minutes to regenerate a token. Waiting until you’re “sure” before rotating is backwards. Rotate first, investigate after.

npm audit still runs, but I don’t fully trust it. It’s good for formally disclosed, database-tracked stuff, but a lot of recent supply-chain incidents are social engineering against a maintainer or a stolen publish token — not a code vulnerability that gets auto-scanned. Audit can come back spotless while something’s actively wrong and just hasn’t been logged yet. So I still follow a couple manual advisory feeds on the side.

Postinstall scripts, more than the dependency code itself

The pattern I keep reading about lately runs through a script that fires automatically on npm install, not through code that actually gets imported. For a new project, or a dependency I don’t use much, I’ve gotten into the habit of peeking at package.json for a preinstall or postinstall script before actually installing. If there’s one and I’m not sure why it’s there, --ignore-scripts first, read the script, enable it only once I’m satisfied it’s legit.

Client work makes this harder, not easier. My own repos are easy to keep disciplined. Client codebases often have lockfiles nobody’s refreshed in a while, dependencies nobody’s audited, inconsistent rules about who can add a new package. I can’t force my own standards on every project I touch, but I do run this check the moment a major incident breaks, and if there’s real exposure I tell the client — staying quiet because it’s awkward to flag someone else’s repo isn’t really an option.

I’ve also gotten stingier about adding dependencies than I used to be. Not blanket distrust of open source — just that every new dependency expands an attack surface I don’t directly control. Before pulling in a package for something I could write myself in twenty lines, I think twice now.

Keeping notes so I stop relearning the same lesson

At first there was no system — every incident handled from scratch, trying to remember what I did last time. Now there’s one plain markdown file per client (or one covering all my personal projects) with the date, the affected package, what I did, how long it took. Nothing fancy. But it means the next incident gets checked against a pattern instead of a blank page — that’s how I noticed three of my last five incidents came through the same transitive dependency, which pointed at one recurring weak spot worth extra attention.

I also overcorrected for a while. In the first few months of taking this seriously, every GitHub security advisory notification made me drop everything to investigate, even for a dev dependency that never touched the production bundle. Took a few months to recalibrate — now I check production vs. dev-only, direct vs. buried-three-levels-deep transitive, before deciding how urgent something actually is.

None of this takes long — maybe twenty, thirty minutes across all my projects, per incident. Cheap compared to cleaning up a leaked token or a deploy that stayed quietly compromised for weeks without anyone noticing.

It doesn’t make me immune. Risk at npm’s scale doesn’t go away, only gets managed. But after a few big incidents this year, I trust this boring, consistent routine a lot more than hoping my dependencies never happen to be next.