What happened in May 2026
Two Microsoft reports in the same month made this hard to ignore. On the 28th, the research described typosquatted npm packages being used to steal cloud and CI/CD secrets. The next day came the report of 33 malicious npm packages abusing dependency confusion to profile developer environments.
These are two distinct vectors that hit the same weak spot: how your build resolves dependencies. Once you understand how each one works, the defense becomes almost obvious. The hard part is that almost nobody audits package resolution until an incident forces them to.
Dependency confusion, demystified
The idea is simple, and that is exactly why it is dangerous. Your company has an internal package, say acme-auth, published to a private registry. The build knows to fetch acme-auth, but it does not always know from where. If that name is not pinned to a prioritized private registry, the resolver can reach the public registry and find a namesake published by an attacker.
The public package often wins by carrying a higher version. The build pulls the impostor by mistake, runs its postinstall, and that is it: attacker code executes inside your environment. In the 33 reported packages, the goal was to profile the developer environment, meaning collect data about the machine and the pipeline to set up the next move.
Note the detail: no server was breached, no password leaked. The attacker simply published a name your build was already looking for.
Typosquat, the look-alike neighbor
Typosquat is the classic typo trap. The attacker registers expres, lodahs, react-domm, names one character away from packages everyone uses. A tired dev fat-fingers an npm install, a CI script carries an old typo, and the malicious package lands in the tree.
In the May case, these typosquatted packages were designed to steal cloud and CI/CD secrets. That tracks: CI is where secrets live in usable, plaintext form at build time.
Why CI is a prime target
Think about what a CI runner holds when it runs an install:
- Cloud tokens for deploy (AWS, GCP, Azure).
- Registry credentials to publish images and packages.
- Signing keys, environment variables, an
.npmrcwith a token. - Network access to internal services.
And in most pipelines, the postinstall of any dependency runs with that same context, no sandbox. A malicious package in the tree reads process.env, finds the deploy token, and exfiltrates it in a network request before your tests even run. The attack does not need to survive your test suite, it happens at install time.
The defenses, prioritized
I ordered these by return on effort. The first few cut most of the risk with little change.
1. Lock the registry per scope in `.npmrc`
This is the cheapest defense against dependency confusion. Put your internal packages under a scope (@acme/) and tie that scope to the private registry:
@acme:registry=https://registry.internal.acme.com
//registry.internal.acme.com/:_authToken=${NPM_TOKEN}
Now @acme/auth can only come from the private registry. There is no public namesake of a scope you own, so the confusion vector simply closes for those packages.
2. Use scoping and namespaces on every internal package
An unscoped internal package is a contestable global name. Migrate acme-auth to @acme/auth. The scope is registered and controlled by you, which removes the option for anyone to publish the same name on the public registry. Worth the rename.
3. Private registry with priority, not as a fallback
Configure your proxy or private registry as the primary source, not as a mirror consulted only when the public one fails. Resolution order matters. If the public registry is checked first, confusion walks right back in through the side door.
4. Lockfile with integrity, always
Commit the package-lock.json (or your manager's lockfile) and install with npm ci, not npm install, in CI. npm ci honors the lockfile and fails if anything diverges. The integrity field (a per-artifact SHA-512 hash) guarantees the byte you receive is the byte you pinned. A version swapped underneath you will not pass.
5. Name verification against typos
Before adding a dependency, check the name carefully. Tooling helps, but the human rule is simple: be suspicious of a new package with few downloads, published recently, with a name one character off a popular one. A dependency scanner (RET offers AuditMySaaS) catches this kind of signal early, before the package hits your tree. It does not replace review, it moves it earlier.
6. Secrets via OIDC and short-lived tokens
This is the move that turns a compromise into a non-event. Replace long-lived credentials pinned in CI with OIDC federation: the runner exchanges its identity for a temporary, narrowly scoped token that expires in minutes. If a malicious package runs at install and leaks the token, the token is already dead or nearly useless. GitHub Actions, GitLab and the cloud providers support this today.
7. Least privilege on the runner
The CI runner does not need admin access. Give each job only that job's permission: whoever builds does not need deploy, whoever tests does not need production credentials. Narrow token scope, isolate the publish job from the test job, and deny egress network where you can. Less power on the runner means less for an attacker to steal.
8. Contain `postinstall`
Lifecycle scripts are the preferred trigger for these attacks. Where your stack allows, run installs with --ignore-scripts and execute only the scripts you audited. At minimum, know which dependencies run postinstall and why. A utility package that insists on running a script at install time deserves a second look.
An honest note on scanners
No single tool solves this. The configuration defenses (locked scope, prioritized registry, OIDC) are what actually closes the vector. A dependency scanner does not stop the attack, it warns you early, when a suspicious name, a strange new version, or a typo enters your graph. It is a safety net, not a wall. Treat both as layers: configuration that shuts the door, and a scanner that watches the lock.
Where to start today
If you only have an hour, do item 1 and item 6: lock your internal packages' scope in .npmrc and move the CI deploy secret to short-lived OIDC. Those two cut both May 2026 vectors at the root. The rest is incremental hardening, and every item you check reduces the surface in a measurable way.
The attack does not breach your server, it publishes a name your build was already searching for. Close package resolution and the CI secret before someone tests it for you.




