JoeCode
TIL: Your internal npm package name is a loaded gun
Sep 23, 2026I was poking at a web app for a security assessment this week and ran a quick check on one of its dependencies, redacted-generic-plugin-name. Sounds real. Plausible. I curled the registry.
$ curl -s -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/redacted-generic-plugin-name
404
Nobody owns it. Which feels like good news for about three seconds.
The problem
At some point a developer made a local package with that name. Monorepo, internal registry, some npm link they forgot about, whatever. The app’s package.json still asks for it by name.
And npm install doesn’t care where you meant that package to come from. It has a name and a version range, and it asks a registry. Misconfigured pipeline, loose .npmrc, a fresh CI runner nobody set up right — npm shrugs and goes to the public registry: “Got anything called chartjs-plugin-colors?”
Today the answer is “no.” That’s the whole defense.
The attack
This is called dependency confusion, and Alex Birsan used it in 2021 to get code execution inside Apple, Microsoft, PayPal, and a few dozen others. The recipe is embarrassingly short:
- Find an internal package name your target uses. They leak everywhere — public repos,
require()strings in shipped bundles, error messages, job postings. - Publish something with that exact name to npm. Version
99.0.0is traditional. - Wait.
The resolver sees your 99.0.0, the internal 1.4.2, and picks the bigger number. Your postinstall runs on their CI box. That’s it. No phishing, no zero-day. You typed a name into npm publish.
Unclaimed is worse than claimed
Weirdly, the app would be slightly safer if some random hobbyist already owned the name. At least you’d know who you were trusting. An unclaimed name is an empty apartment with your company’s address on the door.
What to do
Scope it. @yourcompany/redacted-generic-plugin-name, and register the scope on npmjs.org so nobody else can. Then pin the scope to your internal registry:
@yourcompany:registry=https://npm.internal.yourcompany.com/
Now that lookup can’t fall through to public npm even when someone forgets.
Or just squat your own name. Can’t rename? Publish an empty package with a README that says “reserved.” Free. Takes two minutes.
Check your lockfile. Lockfiles pin versions, not registries. Look for resolved URLs pointing at npmjs.org for packages that should be internal.
Go look right now. For every dependency you don’t recognize as a well-known public package:
curl -s -o /dev/null -w "%{http_code} %{url}\n" https://registry.npmjs.org/PACKAGE_NAME
Every 404 is a name you’re betting on that nobody’s defending.
Claude assisted with this bit of noise. The 404 was real