Skip to content
Security Notes

Dependency confusion: testing your own organization for it

By Elias Lankinen11 min read

I have strong, well-dated sourcing (Birsan's own disclosure, the Microsoft whitepaper, PEP 708, Sonatype's telemetry, Include Security's technical analysis, the confused tool, CSO reporting) and four verified Wikimedia image URLs. Writing now.

The $130,000 mistake that no one actually typed

In the summer of 2020, a security researcher named Alex Birsan was staring at a file that PayPal's engineers had accidentally left in public. It was a package.json — the manifest that a JavaScript project uses to list its dependencies, the outside code it pulls in to function. Most of the names in it were ordinary open-source libraries anyone could download. But a handful were not. They referenced packages that did not exist on npm, the public registry where the world's JavaScript lives. They were PayPal's own internal code, named and required as if they were public, but never published anywhere Birsan could see. So he published them himself. He registered those exact names on the public npm registry, filled the packages with a small script that phoned home, and waited. Within days, according to Birsan's own account, his code was executing inside PayPal's build systems. PayPal paid him a $30,000 bug bounty. Then he did it again, and again, against every company with a bounty program whose internal package names he could scrape. By the time he published his findings on February 9, 2021, he had run his code inside Apple, Microsoft, Netflix, Uber, Yelp, Shopify, Tesla and more than 35 organizations in total, earning over $130,000 in bounties, several of them $30,000 to $40,000 apiece. He called the technique dependency confusion. The unsettling part is that nothing was hacked in the traditional sense. No password was cracked, no server was breached. Birsan simply exploited a decision that package managers make thousands of times a day, and that almost no one had thought to question.

Why package managers hand the win to the highest bidder

To see why this works, you have to understand what a package manager is actually doing when you ask it to install something. Tools like npm (for JavaScript), pip (for Python), and RubyGems (for Ruby) take a list of names and versions and go fetch the matching code. Large organizations rarely rely only on the public registries, though. They run private registries as well: internal servers that host proprietary code the company wrote for itself and does not want to give away. The problem is what happens when a name exists in both places. If your build asks for a package called internal-auth-utils, and that name lives on your private server and someone has published a package by the same name to the public registry, which one does the tool install? The answer, for years, was the worst possible one: the package with the higher version number wins, regardless of which registry it came from. Your internal version might be 1.4.2. An attacker who publishes a public package of the same name at version 9000.0.0 will win every time. The build system, trying to be helpful and keep you on the latest release, reaches past your own code and installs the stranger's. Each ecosystem gets there by a slightly different route:

  • Python's pip has a flag called --extra-index-url. As the name suggests, it adds a public index alongside your private one rather than replacing it. pip then considers every configured index and picks the highest version it finds anywhere, which is exactly the behavior an attacker exploits. Using --index-url alone, which replaces the default, is far safer, but --extra-index-url is everywhere in the wild because it looks convenient.
  • npm has no built-in notion of splitting names between a public and private index. As Include Security's analysis explains, the danger comes from the private registry servers organizations put in front of npm, such as the popular open-source Verdaccio, which by default proxy to public npm for updates to internally published packages. Ask for an internal name, and the proxy will happily go check the public registry too.
  • RubyGems allows multiple sources and resolves by version in a comparable way. In Birsan's tests, four of eight internal Ruby gem names he tried at target companies turned out to be claimable. Across all his targets, Birsan published more than 200 packages carrying between 100 and 1,000 versions each, and roughly three-quarters of the callbacks he received came from the npm ecosystem.

The misconception worth killing first: "we run a private registry, so we're fine"

This is the single most common misreading of dependency confusion, and it is dangerous precisely because it is half true. Yes, you need a private registry to have internal packages at all. But a private registry does not protect you on its own. What matters is whether that registry, or the developer tooling in front of it, will also look at the public internet for a name it already holds privately. If it will, and most default configurations will, then the mere existence of your internal package name in a leaked file is enough. Birsan did not need access to your private server. He needed only to know the names of the things on it, and those names leak constantly: in JavaScript bundles accidentally shipped to production, in stack traces captured by error-tracking services, in cached build logs, in Dockerfiles pushed to public repositories, in the occasional package.json left on a public host, exactly as PayPal's was. The second half of the misconception is subtler. Many teams believe scoped packages fix everything. On npm, a scope is the @company/ prefix in a name like @acme/internal-auth-utils. Scopes help a great deal, because only the owner of a scope can publish packages under it on public npm. An attacker cannot register @acme/anything if your organization controls the @acme scope. But that protection holds only if you have actually claimed the scope publicly, and only for packages that are genuinely scoped. One unscoped internal name slipping into a manifest reopens the door.

From proof of concept to /etc/shadow

Birsan was careful. His packages did nothing but report back a small amount of identifying information so he could prove the code had run. He had permission from the companies he tested, and he disclosed responsibly. The people who read his blog post the next morning were not all so principled. Within 48 hours of the February 9 disclosure, Sonatype's automated malware scanners flagged more than 275 copycat npm packages from different authors imitating the technique. The count climbed past 550 within days and eventually well over 700. Measured another way, the week after Birsan's report saw a 7,000% increase in dependency-confusion packages published to npm. And the copies were not benign. Where Birsan's payload phoned home politely, later packages went for the throat. Sonatype documented copycats named after real internal projects at Amazon, Zillow, Slack and Lyft — amzn, zg-rentals, serverless-slack-app, lyft-dataset-sdk — that read and exfiltrated the victim's /etc/shadow file (the hashed passwords for every account on a Linux machine), opened reverse shells, and shipped off the user's .bash_history along with their IP address and hostname. The gap between a research curiosity and a working data-theft tool turned out to be about two days.

Testing your own organization for it

Here is the good news buried in all of this: dependency confusion is one of the rare classes of vulnerability that you can hunt for from the outside, cheaply, and with high confidence, because the test is almost identical to the attack. You are asking one question of every internal package name you use: is this name unclaimed on the public registry? If it is, someone else can claim it. Start by enumerating your own names. Gather the dependency manifests and lockfiles from across your codebase: package.json and package-lock.json for npm, requirements.txt and poetry.lock for pip, Gemfile.lock for Ruby, pom.xml for Maven, composer.json for PHP. Lockfiles matter as much as manifests, because they pin the transitive dependencies your direct ones drag in. Don't forget the names that never appear in a manifest at all: internal packages referenced only in CI/CD pipeline scripts (the automated build-and-deploy systems), Dockerfiles, and developer setup docs. Then check each name against the public registry. The best-known tool for this is confused, an open-source scanner from Visma's product-security team. You point it at a manifest and tell it which ecosystem you're in:

./confused -l npm package.json
./confused -l pip requirements.txt
./confused -l mvn pom.xml

It reads every dependency, queries the public repository for each one, and prints the names it could not find publicly. Those unfound names are your candidate vulnerabilities: internal packages sitting in an unclaimed public namespace. The -s flag lets you exclude namespaces you already control, such as -s '@acme/*', so the report shows only genuine gaps. Snyk publishes a similar tool called snync aimed specifically at catching the risk in source repositories before it ships. A flagged name is not automatically exploitable. Whether an attacker can actually win depends on how your build tooling resolves conflicts, which registries it trusts, and whether the proxy in front of your private server reaches out to the public internet. But an unclaimed internal name is the necessary precondition for every dependency-confusion attack, so a clean scan is a meaningful result and a dirty one is a to-do list.

If you want to prove exploitability, do it carefully

Some security teams want more than a list of unclaimed names; they want to demonstrate that a package would actually execute inside a build. This is where testing your own organization stops being purely passive and starts to carry real risk, so a few rules are worth stating plainly. First, authorization. Even though Birsan had written permission from every company he tested, dependency confusion sits in a legal gray zone, because the moment you publish a package to a public registry it is available to everyone, not just your build. Only test names your organization owns, and get sign-off in writing before you publish anything public. Second, keep the payload benign. A safe proof-of-concept does the minimum needed to show code ran: a single outbound DNS or HTTPS request to infrastructure you control, carrying a hostname and nothing sensitive. It never reads files, never opens a shell, never touches production data. The copycats that grabbed /etc/shadow are the model of what not to build. Third, clean up. A test package you publish to prove a point becomes an unmaintained public package the instant you walk away. Either take it down or, better, convert it into the actual fix described below by turning it into a permanent placeholder your organization controls.

What the ecosystems did, and what they left undone

The response to Birsan's disclosure split along predictable lines: guidance was quick, structural fixes were slow. Microsoft moved fastest with a whitepaper, 3 Ways to Mitigate Risk When Using Private Package Feeds, whose three recommendations remain the clearest defensive checklist available. Use a single private feed that pulls in any public packages you need, so your build never has two indexes to be confused between. Control your scopes and names, either by using an ecosystem's scoping feature or by defensively registering your internal names as empty placeholder packages on the public registry so no attacker can claim them. And use client-side verification — pin exact versions and enforce integrity hashes (pip's --require-hashes, npm's lockfile integrity fields) so an unexpected substitute aborts the build instead of installing silently. Registry managers like JFrog Artifactory, Sonatype Nexus and Cloudsmith added pattern-based controls that let you block public packages matching your internal naming conventions outright, which is the enterprise version of controlling your scope. The deeper fix — teaching pip itself to stop guessing — proved harder. In February 2023 the Python community proposed PEP 708, a change to the repository protocol so that when pip found the same project available from more than one repository, it would refuse to proceed and raise an error rather than silently pick the highest version. Repositories that legitimately mirror each other could opt to declare the link. The proposal sat in provisional acceptance for three years. On April 2, 2026, it was rejected: the conditions for adoption — coordinated implementation across PyPI, a second repository, and pip — were never met. The most direct attempt to fix dependency confusion at the level of the tool, rather than the configuration, quietly failed less than six months ago. That failure is the thing to watch. More than five years after Birsan published, the reason dependency confusion persists is not that the defenses are unknown; it is that they are opt-in. The Microsoft checklist works, scoping works, defensive registration works, but each requires an organization to notice the problem and act, and the default behavior of the most-used tools still favors the stranger's higher version number. Every new internal package a growing company creates is a fresh name that may or may not have been claimed publicly, and the only way to know is to check. So the real question dependency confusion leaves you with is not whether your ecosystem has been patched. It hasn't, quite. It's whether anyone at your organization has run the scan this quarter — and whether they'll remember to run it again after the next hundred packages ship.

Sources

  1. Alex Birsan / Medium — Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies (2021)
  2. Cybersecurity Dive — Open source blind trust the culprit in ethical breach of 35 companies (2021)
  3. Sonatype — Dependency Hijacking Software Supply Chain Attack Hits More Than 35 Organizations (2021)
  4. Sonatype / Security Boulevard — Sonatype Spots 150+ (later 275+/700+) Malicious npm Packages Copying Recent Supply Chain Attacks (2021)
  5. Sonatype — Malicious Dependency Confusion Copycats Exfiltrate bash_history and /etc/shadow Files (2021)
  6. Include Security — Dependency Confusion: When Are Your npm Packages Vulnerable? (2021)
  7. Microsoft — 3 Ways to Mitigate Risk When Using Private Package Feeds (2021)
  8. CSO Online — Dependency confusion explained: Another risk when using open-source repositories (2021)
  9. Python Enhancement Proposals — PEP 708 – Extending the Repository API to Mitigate Dependency Confusion Attacks (created 2023, rejected 2026)
  10. Visma Product Security — confused: Tool to check for dependency confusion vulnerabilities in multiple package management systems (2021)
  11. Snyk — Detect and prevent dependency confusion attacks on npm to maintain supply chain security (2021)