Vulnerability

Dependency Confusion
detect, understand, remediate

Dependency confusion is the supply-chain attack class where an attacker publishes a malicious package to a public registry under the same name as an internal package, and the build pipeline silently resolves to the attacker package. The Alex Birsan 2021 disclosure landed inside Microsoft, Apple, PayPal, Shopify, Tesla, Uber, and Yelp; the root cause is not a CVE in a dependency, it is the resolver order, the scope rule, and the lockfile pin that the build configuration did or did not enforce.

No credit card required. Free plan available forever.

Severity

Critical

CWE ID

CWE-427

OWASP Top 10

A06:2021 - Vulnerable and Outdated Components

CVSS 3.1 Score

10.0

What is dependency confusion?

Dependency confusion is the supply-chain attack class where an attacker publishes a malicious package to a public registry under the same name as an internal, private, or unscoped package used inside an organisation, and the build pipeline silently resolves to the attacker package instead of the legitimate internal one. The fault is not a CVE in a dependency; it is the resolver order, the scope rule, the registry routing policy, and the lockfile pin that the build configuration did or did not enforce. The attack is published in plain sight, indexed by the registry, and pulled by the next build that touches the affected manifest.

The class was disclosed at scale in February 2021 by Alex Birsan, who landed working code execution inside Microsoft, Apple, PayPal, Shopify, Tesla, Uber, and Yelp using only public-registry uploads of names harvested from leaked manifests, GitHub spelunking, and CDN scraping. The harvest used package.json, requirements.txt, internal documentation, JavaScript bundles, and stack-trace messages. The publish step was trivial: register the harvested internal name with a higher version number on the public registry; wait for the next build. The remediation depended on the resolver rules of every affected build, not on a CVE database.

This page covers the resolver-order attack pattern. For the per-package CVE shape that vulnerable dependencies covers and the OWASP A06 umbrella for the broader supply-chain failure pattern that vulnerable and outdated components (A06:2021) wraps, read the matching explainers. Dependency confusion sits inside A06 mechanistically but the fix is structurally distinct: it is a build-configuration and registry-routing change, not a version bump.

How the attack works

1

Harvest internal package names

The attacker collects internal package names from leaked manifests, GitHub commit history, JavaScript bundles loaded by public web apps, error stack traces, CDN-cached source maps, Docker image layers, npm install logs in CI output, and accidentally public documentation pages. The harvest does not need source-code access; the names alone are enough.

2

Publish to the public registry

The attacker registers the harvested name on the matching public registry (npm registry, PyPI, RubyGems, Maven Central, NuGet Gallery, Packagist, crates.io) with a version number higher than the internal release stream uses. The package contains the malicious payload in an install or post-install script.

3

Build resolver picks the wrong source

A subsequent build (developer machine, CI pipeline, deployment image rebuild) runs the package manager install. The resolver evaluates internal and public registries in an order the build configuration set; if the public registry is consulted before the private registry or the internal-scope rule is missing, the higher public version wins and the malicious package installs.

4

Payload executes under build identity

The install or post-install script runs with the privileges of the build process, with access to environment variables (cloud credentials, API tokens, signing keys), filesystem access to the source tree, and network egress to the attacker callback host. Birsan-class proofs of concept exfiltrated hostnames, usernames, and DNS resolution evidence; a real attacker would chain to credential theft and lateral movement.

Where it shows up by ecosystem

Every package manager has a registry-resolution model that the build configuration controls. The control surface differs by ecosystem, and the same risk arrives through different settings. The exposure map below is the per-ecosystem shape an AppSec or DevSecOps team has to cover.

npm and Node.js

The default registry resolves unscoped names against the public npm registry. A bare internal name without an @scope, without a private registry pinned in .npmrc, and without a scoped publish rule is exposed. Scoped packages add a barrier but only if the scope is registered on the private registry first.

Python and pip

pip evaluates --index-url and --extra-index-url in an order where any responding index wins by version. A private index listed alongside PyPI without --index-url-only enforcement or a per-package routing rule (such as devpi pinning or an internal proxy) exposes every internal name on PyPI.

Java and Maven, Gradle

Maven Central and an internal Artifactory or Nexus group repository can both be in the resolver list. Without a mirror policy that forces internal coordinates through the private repository or a group-level filter that blocks public lookup for internal groupIds, the resolver picks the higher public version.

Ruby and RubyGems

Gemfile source declarations control where each gem is fetched. A bare source rubygems.org line and an internal gemserver alongside it without a source block isolating internal gems leaves internal gem names resolvable from the public registry.

PHP and Composer

Composer evaluates configured repositories in order; the first match wins on a per-package basis only when packagist.org is the last entry. A misordered repositories list or a missing exclude rule on the public Packagist mirror exposes internal vendor names.

.NET and NuGet

NuGet.config consumes both the public NuGet Gallery and the private feed. Without a packageSourceMapping section that pins each package id to the source it must come from, the resolver consults every source and the highest version wins. Azure Artifacts upstream policy needs an explicit deny-on-conflict rule to be safe.

Common causes

Internal package names leaked outside the perimeter

Manifests committed to public repositories, JavaScript bundles served on public web apps, CDN-cached source maps, error stack traces, internal documentation accidentally published, leaked CI logs, and Docker image layers all expose the names. Treat any internal name as compromised once it appears in a public artefact, because public-registry name-squatting is irreversible.

Resolver consults the public registry for internal names

The build configuration does not pin internal names to the private registry through scope, packageSourceMapping, source blocks, or repository ordering. Every resolution touches both registries and the higher version wins regardless of where it came from.

No version pinning or lockfile discipline

Manifests use floating ranges (^1.2.3, ~1.2.3, latest) without committed lockfiles, so a fresh install on a CI runner accepts a higher version published a minute earlier. Reproducible builds via lockfiles do not solve the publish window but they remove the silent-upgrade surface that publish window exploits.

No mirror or proxy enforcement

CI pipelines fetch from public registries directly rather than through an internal proxy that allow-lists package coordinates. Without a proxy, there is no audit trail of which packages crossed the perimeter, and no chokepoint to block a new internal-namespace publish before it lands in a build.

No private registry priority enforcement

The private registry exists but the configuration lets the public registry serve internal names whenever the private one returns a 404 or a lower version. Tools like JFrog Artifactory virtual repositories, Sonatype Nexus group repositories, and Azure Artifacts upstream policy support priority but require an explicit rule to enforce it.

Transitive resolution bypasses the rule

Even when the top-level manifest is locked to internal names, a third-party dependency may declare a transitive that resolves against the public registry. Lockfile generation walks transitives, so the deny-on-conflict rule needs to apply across the entire resolved tree, not just direct names.

How to detect it

Detection covers two surfaces: the configuration surface (does the resolver have a rule that protects every internal name), and the publish surface (has anyone already squatted an internal name on a public registry). A programme that covers only one of the two leaves the other open.

Automated and tooling signals

  • SecPortal code scanning runs Semgrep SAST rule packs and dependency analysis against connected repositories, so manifests that declare bare internal names without a scope, package-source-mapping entry, or pinned source block surface alongside the per-package CVE findings on the same engagement record.
  • Repository connections via GitHub, GitLab, and Bitbucket OAuth give the scan access to every manifest path (package.json, .npmrc, requirements.txt, pip.conf, Gemfile, pom.xml, build.gradle, composer.json, NuGet.config) where the resolver rule lives.
  • Bulk finding import lands the output of outside SCA tooling (Snyk, Trivy, Grype, Socket, Phylum, OWASP Dependency-Check, OSV-Scanner) and dependency-confusion checkers through the Nessus, Burp, and CSV parsers so external findings share the same engagement record and lifecycle as the native ones.
  • Continuous monitoring schedules (daily, weekly, biweekly, monthly) keep the scan running after the engagement closes, so a new manifest commit that drops a scope rule or adds an unsafe registry source still lands a finding on the canonical record.
  • Findings management captures CWE-427, the affected manifest path, the package name, the manager (npm, pip, Maven, NuGet, RubyGems, Composer), the resolver rule that is missing, and a CVSS 3.1 vector that reflects the worst plausible impact for the build identity at risk.

Programme-stage review

  • Maintain an internal-namespace allow-list and audit every internal package name against the public registries on a continuous cadence. The audit answers one question: is anyone already publishing under this name. A positive match is an immediate incident, not a backlog finding.
  • Pre-register every internal namespace on the public registry as a defensive squat. Owning the public name even when you never intend to publish to it forecloses the substitution path for that exact name. Combine with scoped namespaces where the ecosystem supports them.
  • Review CI runner egress logs for public-registry traffic that should not exist. A build that should fetch only from the internal proxy but resolves a package from registry.npmjs.org or pypi.org indicates either a missing routing rule or a transitive that bypassed it.
  • Validate lockfile integrity per build. A reproducible lockfile per branch, generated and signed inside the protected build perimeter, removes the silent-upgrade window where a new public version can sneak into a fresh install before code review notices.
  • Track public-registry upload-watch streams (npm follow database, PyPI events feed) for new packages matching internal-namespace patterns. A name that appears on the public side hours after an internal repository commit is a strong substitution signal.

How to fix it

Pin every internal package to a private registry through explicit routing

Configure each package manager so internal names resolve only from the private registry and never from a public mirror. Use npm scopes plus .npmrc registry routing, pip --index-url with a single internal index, Maven mirror entries that force internal groupIds through the private repository, NuGet packageSourceMapping per package id, Gemfile source blocks per gem, Composer repositories ordering with a packagist exclude, and Cargo registries with per-package source rules.

Use scopes, namespaces, or groupId prefixes for every internal package

A scoped or namespaced name (such as @yourorg/internal-package or com.yourorg.internal) is structurally easier to route than a bare name. Combined with public-registry pre-registration of the scope or namespace, scopes shrink the attack surface to packages that an attacker can publish under a new namespace, not under your existing one.

Commit and verify lockfiles for reproducible builds

package-lock.json, yarn.lock, pnpm-lock.yaml, Pipfile.lock, poetry.lock, Gemfile.lock, composer.lock, packages.lock.json, and Cargo.lock all encode the exact version and source for every direct and transitive dependency. Generate the lockfile inside the protected perimeter, commit it, and fail the build if the lockfile diverges from the manifest. The lockfile closes the silent-upgrade window.

Pre-register internal namespaces on every relevant public registry

Claim each internal namespace on the public registries you depend on (npm, PyPI, RubyGems, Maven Central, NuGet Gallery, Packagist, crates.io). Owning the public name even with an empty placeholder package forecloses the substitution path for that exact name. The cost is registry hygiene; the benefit is permanent name protection.

Route all builds through an internal package proxy with allow-listing

Stand up JFrog Artifactory, Sonatype Nexus, Verdaccio, devpi, or Azure Artifacts as a proxy that allow-lists upstream packages. CI runners and developer machines configure the proxy as the only registry, and every public-registry fetch goes through it. The proxy creates an audit trail and gives the security team a chokepoint to block a substituted name before it lands in a build.

Enforce package integrity and signature checks where the ecosystem supports them

Use npm package integrity (sha512) checksums in lockfiles, pip --require-hashes mode, Maven GPG signature validation, NuGet package signing, RubyGems signed gems, Sigstore-signed artefacts where supported, and Cargo Crates.io verified publishers. The integrity check ensures the file you resolved is the file you reviewed.

Treat any public-registry match on an internal name as an immediate incident

A search of npm, PyPI, RubyGems, Maven Central, NuGet, Packagist, and crates.io for every internal package name should return zero matches outside your own published packages. Any positive match is a substitution candidate. Route it as a high-severity finding, identify the publisher, and request a takedown through the registry abuse channel while you mitigate the resolver rule.

Run continuous monitoring on the dependency surface, not just at release boundaries

A scoped scan that runs at every CI build catches manifest drift, new unsafe sources, and accidentally-introduced bare names. A continuous monitoring schedule on top of the per-build scan catches the case where the repository sat untouched but the public registry gained a new substitution candidate against an existing internal name.

How SecPortal records and tracks a dependency confusion finding

SecPortal is not a standalone SCA or supply-chain integrity platform, does not host a private package registry, and does not run a real-time public-registry watch service. It is the workspace where every dependency-confusion finding lands on the engagement record alongside source-level findings, external attack-surface findings, and authenticated DAST findings, so severity, ownership, evidence, fix, and retest stay on one canonical record per service.

Connect the repository through OAuth

Use a GitHub, GitLab, or Bitbucket OAuth connection to give the platform read access to the codebase. Encrypted credential storage holds the OAuth token and the connection is scoped to the workspace so only authorised team members can trigger a scan against the connected repository.

Run code scanning with dependency analysis

Code scanning executes Semgrep SAST and dependency analysis against the connected repository. The pass walks every manifest path (package.json, .npmrc, requirements.txt, pip.conf, Gemfile, pom.xml, build.gradle, composer.json, NuGet.config, Cargo.toml) and surfaces resolver-routing patterns that expose internal names to public-registry substitution.

Record the finding against CWE-427 and OWASP A06

The finding lands in findings management with CWE-427, OWASP A06:2021, the affected manifest path, the package manager, the internal package name, the missing or unsafe resolver rule, and a CVSS 3.1 vector calibrated for the build identity at risk rather than the upstream base score.

Import results from outside supply-chain checkers

Bulk finding import lands Snyk, Trivy, Grype, Socket, Phylum, OWASP Dependency-Check, OSV-Scanner, and bespoke dependency-confusion checker output through the Nessus, Burp, and CSV parsers when an outside tool is the source of truth for an ecosystem the native scan does not cover.

Schedule continuous monitoring

Continuous monitoring re-runs the scan daily, weekly, biweekly, or monthly so a new commit that drops a scope rule, adds an unsafe registry source, or introduces a new bare internal name surfaces on the canonical record without anyone manually re-triggering the scan.

Route ownership through team management RBAC

Assign dependency-confusion findings to the named owner of the affected repository, service, or platform tier through team management roles (owner, admin, member, viewer, billing). The activity log captures the assignment so the audit trail of who took ownership of which finding when is preserved on the record.

Capture exception decisions on the finding

When the response to a finding is a documented accepted risk (for example, a legacy ecosystem with no scope support and a planned migration), the decision lives on the finding through the finding-override exception register pattern rather than in a meeting note. The compensating controls, the re-evaluation date, and the named approver all travel with the record.

Verify closure through retest

The retesting workflow pairs the retest to the original finding so closure means the rescan against the updated manifest, lockfile, or registry configuration no longer reports the unsafe routing. The verified_at and resolved_at timestamps preserve the audit chain of when the configuration changed and who confirmed it.

Generate evidence packs through AI report generation

AI report generation turns the dependency-confusion backlog and closure cadence into an exported report aimed at engineering leadership, AppSec programme review, or audit without rebuilding the narrative from spreadsheets each cycle. The report cites the underlying findings rather than fabricating numbers.

What SecPortal does not do

Honesty on capability matters when the topic is supply-chain integrity. SecPortal does not host a private package registry, does not proxy package-manager traffic, does not enforce a registry routing policy at build time, does not register defensive squat packages on public registries on your behalf, does not subscribe to or stream npm follow events or PyPI event feeds, does not run a real-time public-registry watch service for new uploads matching internal namespace patterns, does not federate against an asset inventory or CMDB, does not integrate with Jira, ServiceNow, Slack, SIEM, SOAR, or upstream registry administrative APIs through a packaged connector, does not orchestrate registry abuse-channel takedown requests, and does not sign packages or build artefacts. Programmes that need real-time public-registry watching, defensive name registration at scale, or integrated proxy operations typically run those activities through purpose-built tooling (JFrog Artifactory, Sonatype Nexus, Socket, Phylum, Snyk, OSV-Scanner cron jobs, or in-house registry watchers) and land the resulting findings on the engagement record through bulk-finding-import. The platform value is the consolidated record where every dependency-confusion finding (whether it came from native code scanning, bulk import from an outside checker, or a manual entry against a discovered public-registry substitution candidate) lives alongside the rest of the security backlog with the same lifecycle, the same RBAC, the same activity log, and the same evidence trail.

For the operating discipline that takes a flagged manifest from detection to a verified closure (intake the finding, validate the resolver rule gap, route to the manifest owner, apply the registry routing or scope fix, regenerate the lockfile, re-run the scan, and verify with a retest against the updated branch), read dependency vulnerability triage. For the inventory layer that lets the response to a Birsan-style disclosure be a query rather than a forensic exercise (which services, which manifests, which registries, which internal namespaces), pair it with SBOM management and VEX publishing.

Dependency confusion sits inside the broader OWASP A06 category but the fix is mechanistically distinct from a CVE-driven version bump; it is a build-configuration and registry-routing change. For the per-package CVE shape, see vulnerable dependencies. For the wider A06 parent that wraps unmaintained, end-of-life, runtime, framework, and container base image risk alongside per-package CVEs, see vulnerable and outdated components (A06:2021).

Compliance impact

Related vulnerabilities and recommended reading

Dependency confusion is one entry in a wider cluster of software-supply-chain attack patterns. The pages below cover the adjacent per-package and ecosystem-wide failure modes, the framework controls that map evidence against the resolver-routing discipline, and the SecPortal capabilities that record the findings.

  • Vulnerable dependencies the per-package CVE finding shape that runs alongside dependency confusion on the same SCA pass, with detection, lockfile, and patch guidance.
  • Vulnerable and outdated components (A06:2021) the OWASP parent category that wraps dependency confusion alongside per-package CVEs, EOL runtimes, container base images, and OS packages.
  • LLM supply chain vulnerabilities (LLM03) the AI artefact analogue of the package-substitution attack: model checkpoints, fine-tuned weights, LoRA adapters, and inference SDK packages that travel the same registry-routing surface.
  • Hardcoded secrets an adjacent finding that often pairs with a dependency-confusion incident when the post-install payload exfiltrates committed credentials from the build environment.
  • Insecure deserialization the runtime-side exploitation pattern that frequently follows a successful substitution, where the malicious package delivers a payload through an unsafe loader.
  • NIST SSDF (SP 800-218) PS.3 (protect components) and PW.4 (acquire and maintain well-secured software) are the practice mappings for component provenance evidence.
  • NIST SP 800-161 (C-SCRM) the cybersecurity supply chain risk management practices that wrap registry-routing decisions inside an enterprise C-SCRM operating model.
  • CISA Secure by Design the federal pledge that asks software producers to verify component integrity, publish SBOMs, and document vulnerability handling for components they ship.
  • Software supply chain security guide the practitioner-side guide to running an end-to-end supply-chain programme that holds dependency confusion, malicious packages, and provenance attestation in one operating model.
  • Software bill of materials guide the SBOM operating guide that gives the response to a public-registry substitution incident the inventory it needs to scope blast radius across services.
  • SLSA framework explained the build-integrity scaffold that pairs each release with a signed provenance attestation so the components in production trace to a known build pipeline.
  • SAST vs SCA code scanning the technical comparison between source-code analysis and dependency analysis inside the same build pass, with the resolver-routing review sitting inside the SCA side.
  • CISA secure software development attestation guide the US federal attestation that requires SBOM, vulnerability disclosure, and component-handling evidence from software suppliers to federal agencies.
  • Reachability analysis vulnerability prioritisation the prioritisation technique that separates exploitable substitution candidates from manifests where the resolver rule already deflects the public lookup.
  • Dependency vulnerability triage the engagement workflow that takes a flagged manifest from detection through registry-routing fix and verified retest, with reachability evidence on the finding record.
  • SBOM management and VEX publishing the inventory layer that lets the response to a Birsan-style disclosure be a query rather than a forensic exercise across services, manifests, and internal namespaces.

Catch internal package names exposed to public registry substitution

SecPortal records dependency-confusion findings against the affected repository, captures the manifest path and registry-routing evidence, lands outside SCA output through bulk import, tracks fix verification through retest, and keeps the closure visible on the engagement record. Start scanning for free.

No credit card required. Free plan available forever.