Safeguard
Open Source Security

npm dependency confusion attacks against private package ...

How npm dependency confusion lets attackers hijack internal package names on public registries — and why private npm registry security gaps still leave teams exposed.

Aman Khan
AppSec Engineer
7 min read

In February 2021, security researcher Alex Birsan published a technique that made hundreds of engineering teams quietly check their build logs. By uploading public packages that matched the internal package names used inside companies like Apple, Microsoft, PayPal, Netflix, Uber, Tesla, and Shopify, he got his code auto-installed inside their production and CI environments. No phishing, no stolen credentials, no exploited zero-day — just a naming collision between a private package and a public one. The technique, now known as npm dependency confusion, earned Birsan more than $130,000 in bug bounties, including a single $30,000 payout from PayPal, and it exposed a structural flaw in how npm, pip, and other package managers resolve dependencies by default. Five years later, the underlying weakness — public registries trusting whichever version number is highest — is still present in a lot of build pipelines, and private npm registry security is still an afterthought for many teams. Here's how the attack works and what actually stops it.

What Is npm Dependency Confusion?

npm dependency confusion is an attack where a public package on the npmjs.com registry is given the same name as a private, internally-published package, tricking build tools into installing the attacker's public version instead of the trusted internal one. The root cause is dependency resolution logic: when a company uses a mixed setup — some packages pulled from the public npm registry, others from an internal registry like Artifactory, Verdaccio, or GitHub Packages — many configurations resolve by simply picking the highest semantic version number across every reachable source, public or private. If an internal package is versioned 1.3.2 and an attacker publishes a public package of the same name at version 9.9.9, npm will often install the attacker's package, because nothing about the resolution logic checks where a package came from — only what version it claims to be.

This is different from typosquatting, where attackers register lodahs hoping for a fat-fingered npm install. Dependency confusion doesn't need a typo. It needs only that the attacker learn the exact, correctly-spelled name of a package your company already trusts and never intended to expose.

How Did Birsan's 2021 Research Change the Threat Model?

It proved the attack worked at scale against real production systems, not theoretical ones. Birsan didn't guess internal package names — he found them. Leaked package.json files in public GitHub repositories, internal file paths visible in error messages and JavaScript source maps, and even employee resumes referencing internal tooling all leaked the exact strings he needed. Across 35+ companies he tested, most had no naming convention that separated internal packages from anything a stranger could register on the public registry. Within days of disclosure, npm and PyPI both moved to let organizations reserve scoped namespaces, and companies including Microsoft published guidance recommending scoped packages (@yourcompany/package-name) as a baseline defense. But scoping only helps if it's enforced everywhere — in CI configs, in .npmrc files, and in every developer's local environment — and audits since 2021 have repeatedly found unscoped internal packages still sitting in the wild.

Why Do Private npm Registry Security Gaps Make This Attack Possible?

Because most organizations run a hybrid setup where public and private feeds are treated as equally trustworthy sources for the same namespace. A typical internal package registry proxies or falls back to the public npm registry for anything it doesn't already host — which is convenient for onboarding open-source dependencies but dangerous the moment an internal package name isn't explicitly locked down. If your .npmrc doesn't pin every internal scope to your private registry with always-auth and a scoped registry mapping, a developer's laptop, a CI runner, or a Docker build stage can silently resolve that name from npmjs.com instead. This is exactly the kind of internal package namespace attack that doesn't require breaching your network — it only requires the attacker to publish faster and with a higher version number than whatever's sitting in your private feed. GitHub's own security advisories and multiple 2022–2023 red-team engagements found that even companies aware of the 2021 disclosures still had at least one unscoped internal name reachable through default resolution.

What Does an Internal Package Namespace Attack Look Like Step by Step?

It looks like a normal npm install — that's what makes it dangerous. First, the attacker identifies an internal package name, often from a leaked package-lock.json, a public CI log, a job posting mentioning internal tooling, or even LinkedIn profiles listing project names. Second, they register a public npm package with that exact name and publish it at an artificially high version, sometimes with a postinstall script that runs automatically on install — no execution step required by the victim. Third, somewhere in the target's pipeline — a laptop without proper registry scoping, a stale CI cache, a contractor's machine — the build resolves the public package instead of the internal one, and the postinstall script runs with whatever privileges that build environment has: reading environment variables, AWS credentials, SSH keys, or CI secrets, and exfiltrating them to an attacker-controlled endpoint. The entire compromise can complete before a human ever looks at a diff. This mirrors what happened in the PyTorch-nightly incident of December 2022, where a malicious torchtriton dependency uploaded to PyPI harvested SSH keys and environment data from anyone who installed the nightly build — a different registry, same underlying flaw.

How Is This Different From Other Node.js Supply Chain Attacks?

Dependency confusion is distinct because it exploits trust in naming and resolution order rather than exploiting a vulnerability in code itself. Typosquatting relies on human error. Malicious maintainer takeovers — like the 2018 event-stream incident, where a popular npm package was handed to a new maintainer who quietly added a Bitcoin-wallet-stealing dependency — rely on social engineering and abandoned-project handoffs. Protestware and post-install cryptomining scripts rely on a package already being installed and then behaving badly on update. Dependency confusion needs none of that: it targets the moment before trust is established, at resolution time, using a name the victim already considers safe because they wrote it themselves. That's what makes it uniquely dangerous as a node.js supply chain attack category — it turns your own internal naming conventions into the attack surface, and it scales effortlessly because one squatted name can be registered once and silently wait for any of hundreds of engineers to trigger an install.

How Safeguard Helps

Safeguard closes the exact gap that makes npm dependency confusion possible: the moment where a build can't distinguish a trusted internal package from a lookalike public one. Safeguard continuously maps every internal package namespace your organization uses — across npm, PyPI, and other ecosystems — and monitors public registries for squatting attempts the instant a matching name appears, so you're alerted before a malicious version ever reaches a build. Safeguard also audits .npmrc, CI pipeline configs, and registry proxy settings across your repos to flag any place where internal scopes aren't explicitly pinned to your private registry, closing the resolution-order loophole that lets public packages silently win. For every dependency that does enter a build, Safeguard verifies provenance and origin before install, blocking unscoped or unverified packages from executing postinstall scripts in CI. And because dependency confusion often starts with a leaked internal name, Safeguard scans public repositories, CI logs, and package manifests for exposed internal package identifiers — the same reconnaissance signals Birsan used in 2021 — so your team can lock down a namespace before an attacker ever finds it. The result is a supply chain where private and public dependencies are never treated as interchangeable, and where the trust your engineers place in an internal package name can't be silently hijacked.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.