A Live npm Worm Runs Through node-gyp's Build Step, Not the Install Scripts. It's the Clearest Reason Yet to Turn On the npm v12 Default Today.
There's a self-propagating npm worm in the wild right now that doesn't bother with the lifecycle scripts every scanner has learned to flag. It hides in the native build step. A poisoned binding.gyp file tells node-gyp to compile something during npm install, and the compile runs attacker code: no preinstall, no postinstall, nothing in the package's scripts block that would catch your eye in a diff.
Researchers are calling the technique "Phantom Gyp." Snyk is tracking the wave as the Node-gyp Supply Chain Compromise. And the genuinely useful part of this story isn't the attack: it's that the npm change you've been told is coming in July, the one that breaks half your native dependencies, is the exact thing that stops this. You don't have to wait for it.
What Phantom Gyp actually does
The campaign moved fast and quietly. According to StepSecurity, which named the install-time technique, and Snyk's tracking entry, it compromised roughly 57 packages across more than 286 malicious versions, with the first wave starting around June 3. One of the early casualties was @vapi-ai/server-sdk, the official Vapi voice SDK pulling somewhere north of 400,000 downloads a month: not an obscure typosquat, a package real teams depend on.
Here's the mechanism that makes it nasty. Most supply-chain malware rides the preinstall or postinstall lifecycle scripts, because those run automatically and they're trivial to weaponize. That also makes them the first place every security tool looks. Phantom Gyp goes a layer down. It ships a weaponized binding.gyp, the build manifest node-gyp reads to compile native modules. When npm installs a package that has native bindings, node-gyp fires automatically to build them, and the malicious build steps run attacker-controlled code with the permissions of whoever ran npm install.
On a developer laptop or a CI runner, that's a lot of permission. The payload harvests credentials across the obvious targets (npm tokens, GitHub credentials, AWS, GCP, and Azure keys, HashiCorp Vault, Kubernetes config), exfiltrates them through attacker-controlled GitHub repos, and then self-propagates by republishing packages from any maintainer account whose token it just stole. It's the same Shai-Hulud / Miasma worm family that keeps coming back, wearing a new entry point.
Why the install-script defenses missed it
If you've been hardening your install pipeline this year, you probably did the sensible thing and started watching lifecycle scripts. Maybe you run with --ignore-scripts in CI. Maybe you've got a tool that flags any new postinstall. That's good hygiene, and it's exactly why the attackers moved.
A binding.gyp build isn't a declared script. A package can have a completely clean package.json (no scripts section at all) and still trigger a native compile, because npm runs an implicit node-gyp rebuild for any package that has a binding.gyp. So the older mental model, "I'm safe if I block postinstall," had a real hole, and Phantom Gyp drove straight through it.
That's the part worth sitting with. The attack isn't clever because it found a zero-day. It's clever because it noticed where everyone was looking and stepped one foot to the side.
The fix already exists, and it's the thing you were dreading
Here's where the story turns, and where the reporting actually surprised me in a good way.
The npm v12 change that's been making the rounds (install scripts disabled by default, the one that'll force you to approve native builds and that the changelog warns "breaks real packages") covers this. Per GitHub's npm v12 changelog, v12 treats implicit binding.gyp builds the same way it treats declared scripts: blocked by default, surfaced in an approval list. A dependency with a spotless, script-free package.json that quietly wants to run a native build now shows up and asks permission instead of just running.
So the upgrade everyone's been grumbling about isn't only an annoyance that breaks bcrypt and sharp and better-sqlite3 on a fresh install. It's the specific control that neutralizes Phantom Gyp. The friction is the feature.
And you don't have to wait for v12 to ship and get forced on you. You can run the same posture today:
# Install nothing's scripts or native builds automatically... npm install --ignore-scripts # ...then explicitly allow the packages you actually trust to build. npm rebuild bcrypt better-sqlite3 # only the ones you vetted
If you're on a recent npm that has it, npm approve-scripts gives you the interactive allowlist flow that v12 will make the default. The point either way is the same: nothing native compiles on install unless you said so by name.
What I'd actually do this Saturday
Three things, in order, and none of them take long.
First, set ignore-scripts=true in the .npmrc for your projects and your CI, then build an explicit allowlist of the handful of packages that legitimately need a native compile. For most solo projects that list is short: a database driver, a hashing library, maybe an image tool. Everything else has no business running a build step, and now it can't.
Second, check whether you pulled anything in the blast radius. Search your lockfiles for the named packages from the Snyk advisory and any version published in the early-June window, @vapi-ai/server-sdk included. If you installed one on a machine with real credentials, assume exposure: rotate npm tokens, GitHub tokens, and any cloud keys those environments could reach, and look at your GitHub account for repos or Actions workflows you didn't create.
Third, and this is the one that compounds, stop treating the npm v12 upgrade as a chore to defer. Move to the script-blocking default now, on your terms, while it's a calm Saturday decision instead of a 3 a.m. one. The teams that get hurt by the next one of these won't be the ones who turned the control on early. They'll be the ones who kept install scripts wide open for convenience right up until the convenience cost them their AWS keys.
I run with --ignore-scripts everywhere and an allowlist I can count on one hand. It is mildly irritating maybe twice a month, when a new dependency needs a build I have to approve. That's the whole tax. Compared to the alternative, it's nothing.
Author
Lukas
@lukcombinatorSources
- Node-gyp Supply Chain Compromise (Snyk)
- Miasma: Phantom Gyp npm worm via binding.gyp (StepSecurity)
- Upcoming breaking changes for npm v12 (GitHub Changelog)
- binding.gyp supply chain attack compromises dozens of npm packages (Cyber Security News)
- GitHub finally pulls the plug on automatic install script execution for npm (InfoWorld)