Loading...
Skip to main content
Security

Dependency Confusion Attacks: How Attackers Hijack Your Private Packages

Dependency confusion is a supply chain attack where attackers register public packages with the same name as your private internal packages. Your package manager installs the attacker's version instead. Here's how it works, which ecosystems are vulnerable, and what you can do about it.

Sudhir P.
Last updated
11 min read

If your organization uses private packages — internal libraries shared across teams — you may be vulnerable to one of the most effective supply chain attacks discovered in recent years. It doesn't require finding a zero-day. It doesn't require compromising a maintainer's account. The attacker just registers a public package with the same name as your private one, gives it a higher version number, and waits.

Key Takeaway

TL;DR: Dependency confusion attacks exploit how package managers resolve names. When a private package and a public package share the same name, most package managers default to the higher-version public package. Attackers register public packages matching internal names, publish v99.0.0 with malicious code, and your CI/CD pipeline installs it automatically. Scoped packages, registry pinning, and lockfile verification are your primary defenses. Traditional CVE scanners won't catch this — you need supply chain-specific tooling alongside vulnerability scanning.

What We'll Cover

What Is a Dependency Confusion Attack?

A dependency confusion attack exploits a simple ambiguity: when your project depends on a package called auth-utils, which registry does the package manager check first?

Most package managers search public registries by default. If an attacker has published auth-utils on the public registry with a version number higher than your private version, the package manager installs the attacker's package. Your build system pulls in malicious code, thinking it's your own internal library.

The attack is effective because it targets the resolution logic of the package manager itself — not a bug in your code or a vulnerability in a dependency. The package manager is doing exactly what it was designed to do: resolve the highest available version of a package by name.

The term "dependency confusion" was coined by security researcher Alex Birsan in 2021, though the underlying resolution behavior had been known to package manager maintainers for years. What Birsan demonstrated was that this theoretical risk was trivially exploitable at scale against major corporations.

How the Attack Works

Here's the attack step by step, using npm as the primary example.

Step 1: The company creates an internal package. Your engineering team builds a shared library — say, auth-utils — and publishes it to your private npm registry (Artifactory, Verdaccio, GitHub Packages, etc.). Internal projects depend on auth-utils@1.2.3.

Step 2: The attacker discovers the package name. Internal package names leak constantly. Job postings mention them ("experience with our auth-utils library"). GitHub repos reference them in error messages, import statements in open-sourced code, or package.json files that were briefly public. Stack Overflow questions mention them. Slack message previews expose them.

Step 3: The attacker registers the name on the public registry. The attacker publishes auth-utils@99.0.0 on npmjs.com. The package contains a preinstall script that exfiltrates environment variables, hostname, and directory paths to an attacker-controlled server. In more targeted attacks, it installs a reverse shell or modifies build artifacts.

Step 4: The package manager resolves to the public version. When a developer runs npm install or CI/CD triggers a fresh install, the package manager checks both the private and public registries. It sees auth-utils@1.2.3 on the private registry and auth-utils@99.0.0 on the public registry. Following standard semver resolution — highest matching version wins — it installs v99.0.0 from the public registry.

Step 5: Malicious code executes. The preinstall script runs before your code even compiles. It has access to CI/CD secrets, environment variables, SSH keys, and anything else available in the build environment. The attacker now has a foothold in your infrastructure.

The entire attack requires no exploitation of a vulnerability. It's a feature of how dependency resolution works.

Which Ecosystems Are Vulnerable

Not all package ecosystems are equally exposed. The key differentiator is whether the ecosystem supports namespace scoping.

npm — Partially vulnerable. npm supports scoped packages (@company/auth-utils), and scoped names can only be published by the scope owner. But unscoped packages (auth-utils) have no ownership protection — anyone can register any unclaimed name. Organizations using unscoped private packages are fully exposed.

PyPI — Most vulnerable. Python's package ecosystem has no namespace concept at all. Any user can register any unclaimed package name. If your internal pip configuration falls back to PyPI, you're at risk. Combined with Python's setup.py execution during install, this makes PyPI the most dangerous ecosystem for dependency confusion.

RubyGems — Vulnerable. Like PyPI, RubyGems has no namespace scoping. Any unclaimed gem name is available to register. Private gem servers that fall back to rubygems.org are susceptible.

NuGet — Configurable. NuGet supports multiple package sources with configurable priority. The nuget.config file can be locked down to prevent public registry fallback, but the default behavior does check nuget.org. Organizations that haven't explicitly configured source priority are vulnerable.

Go modules — Mostly safe. Go's module system uses URL-based paths (github.com/company/auth-utils). Since the import path includes the hosting domain, an attacker can't register a conflicting name on a different domain. This is inherently resistant to confusion attacks, though proxy configurations (GOPROXY) need proper scoping.

Packagist (PHP) — Mostly safe. Composer packages require a vendor namespace (company/auth-utils). Since the vendor name is part of the package identity, confusion attacks are difficult unless an attacker controls a matching vendor name on Packagist. Private Packagist configurations should still pin the repository source explicitly.

For a broader look at ecosystem-specific security concerns, see our guides on npm security, PyPI security, RubyGems security, NuGet security, and Go module security.

Real-World Examples

In February 2021, Alex Birsan published his research demonstrating dependency confusion attacks against Apple, Microsoft, PayPal, Shopify, Netflix, Tesla, and Yelp. He identified internal package names from public sources and registered them on npm, PyPI, and RubyGems with elevated version numbers and a DNS callback payload.

The results were striking. Code executed on internal build servers at all targeted companies. In Apple's case, the malicious package ran on machines inside their corporate network. Microsoft's internal build systems pulled the public package. PayPal's CI/CD pipeline installed and executed the attacker's code.

Birsan collected over $130,000 in bug bounties from the affected companies. More importantly, the research demonstrated that dependency confusion was not theoretical — it was trivially exploitable against the largest and most security-conscious organizations in the world.

Since 2021, the technique has been adopted by both security researchers and actual threat actors. In 2022 and 2023, multiple campaigns targeted organizations through PyPI confusion attacks. By 2026, the attack vector remains active. Automated scanners now monitor public registries for newly published packages that match known internal naming patterns, but the fundamental resolution behavior in most package managers hasn't changed.

This attack vector is closely related to other supply chain attack patterns including typosquatting, maintainer account compromise, and build system injection.

How to Detect If You're Vulnerable

Start with a simple audit.

List your private packages. Export every package name from your private registry. For npm, this is your Artifactory, Verdaccio, or GitHub Packages inventory. For Python, check your private PyPI server or --extra-index-url targets.

Check each name against the public registry. For every private package name, check whether that name exists on the public registry. On npm: npm view auth-utils. On PyPI: pip index versions auth-utils. If a public package exists with the same name, you're potentially exposed.

Review your package manager configuration. Check .npmrc, pip.conf, nuget.config, and Gemfile source blocks for how registries are prioritized. If your configuration allows fallback to public registries — which is the default in most ecosystems — the confusion attack is possible.

Check version ranges. If your package.json specifies "auth-utils": "^1.0.0", semver resolution will happily match 99.0.0. Pinned versions and lockfiles reduce (but don't eliminate) the risk.

Mitigations

No single mitigation is sufficient. Layer these defenses.

Use scoped packages. On npm, always use @company/package-name for internal packages. Scoped names are owned by the organization — an attacker cannot publish to your scope. This is the single most effective mitigation for npm users.

Pin registry sources explicitly. Configure your package manager to route specific packages or scopes to specific registries, with no fallback:

# .npmrc — route your scope to your private registry only
@company:registry=https://npm.internal.company.com/

For pip, avoid --extra-index-url (which adds a source) and instead use --index-url (which replaces the default) combined with a tool like pip-compile that locks sources per package.

Use a private registry proxy. Tools like Artifactory, Nexus, or Verdaccio can act as a proxy that caches public packages while serving private ones. Crucially, configure the proxy to prioritize private packages over public ones and to block public packages that share names with private ones.

Verify lockfile integrity. Your package-lock.json, poetry.lock, or Gemfile.lock records the exact resolved source for each dependency. Review lockfile changes in pull requests. If a previously-private package suddenly resolves from a public registry, that's a red flag.

Pre-register your internal names. On public registries, register placeholder packages with your internal names (even if they're empty). This prevents an attacker from claiming the name. It's tedious but effective for high-value package names.

Integrate into your CI/CD pipeline. Automate registry source verification as a build step. Block installs if any package resolves from an unexpected source.

Does Your Dependency Scanner Catch This?

Let's be direct: traditional CVE-based vulnerability scanners — including GeekWala — are not designed to detect dependency confusion attacks.

GeekWala scans your dependency tree against the OSV database, enriched with EPSS scores and CISA KEV data, to identify known vulnerabilities in the packages you actually use. If a package version has a published CVE, GeekWala finds it and helps you prioritize remediation.

Dependency confusion is a different class of problem. The malicious package won't have a CVE — it was just published by the attacker. There's no advisory to match against. The attack happens at the resolution layer, before the scanner even sees the dependency tree.

For supply chain attack detection, consider specialized tools: Socket (monitors for suspicious package behavior), Phylum (analyzes package risk signals), or Sigstore/Cosign (verifies package provenance). npm's built-in --before flag and npm audit signatures provide additional signals.

The strongest posture combines both approaches: CVE scanning (GeekWala, Trivy, Grype) for known vulnerabilities in legitimate packages, plus supply chain analysis tools for novel attacks like dependency confusion. Neither alone is sufficient. If you're evaluating tools, our dependency scanner comparison covers the CVE scanning side in detail.

FAQ

Is my company vulnerable to dependency confusion?

If you use private packages and haven't explicitly configured your package manager to never fall back to public registries, the answer is probably yes. The risk is highest if you use unscoped npm packages, private PyPI packages with --extra-index-url, or any ecosystem where namespace scoping isn't enforced. Run the detection steps above to assess your exposure.

Does using scoped packages completely prevent this?

On npm, scoped packages (@company/auth-utils) are strongly resistant because only the scope owner can publish to that namespace. However, scope squatting is a related concern — if your organization hasn't claimed its scope on npmjs.com, someone else could. Claim your scope even if you only publish to a private registry. In ecosystems without scoping (PyPI, RubyGems), scoped packages aren't an option, so you need registry pinning and proxy configuration instead.

What's the difference between dependency confusion and typosquatting?

Typosquatting targets individual developers through misspelled package names — lodassh instead of lodash — hoping someone makes a typo. Dependency confusion targets automated systems through exact name matches with private packages, exploiting version resolution logic. Typosquatting relies on human error; dependency confusion exploits machine behavior. Both are supply chain attacks, but they require different defenses.

Can lockfiles protect against dependency confusion?

Lockfiles help but aren't foolproof. If the lockfile already records the correct private registry source, subsequent npm ci or pip install --require-hashes installs will use the locked resolution. But the lockfile itself was generated at some point — if the confusion attack was active during that initial resolution, the lockfile locks in the malicious package. Always review lockfile changes in code review, and verify that sources match your expected registries.