
What is dependency confusion? How to reduce the risk of attack
Dependency confusion tricks a resolver into installing an attacker's public package instead of your private one with the same name.
Use npm scopes and pip's index-url flag, not extra-index-url, to tie internal packages to one approved private source.
Chainguard Repository routes installs through one policy-managed source, applying cooldowns and malware scans before delivery.
Dependency confusion is a software supply chain attack that exploits how package managers decide where to download a dependency. A build asks for a private package, and the resolver selects a public package with the same name from a source you never approved. If the public package is malicious, its code may run during installation or later, when the application imports or executes the package.
On May 28 and 29, 2026, a threat actor published 45 npm packages with names designed to mimic private package names used by real companies. When a developer’s npm install selected one of those public packages, npm automatically ran its postinstall script, which launched a payload that collected environment variables, hostnames, and other details from the machine. That reconnaissance can expose API keys and access tokens and help an attacker map the environment before the application ever uses the package.
What dependency confusion exploits
An attacker begins by identifying a package name that your build expects to download from a private registry. They then publish a malicious package with the same name to a public registry such as npm or PyPI. If the build’s configuration lets both sources answer for that name, the package manager can select the public package instead of the private one. The attack depends on two conditions. Source ambiguity, the first condition, lets the resolver select the attacker’s package, and the execution path, the second condition, determines when that package’s code runs.
Source ambiguity. Suppose your build needs a private package called
shared-auth-client. The name alone does not tell the package manager whether to fetch it from your private registry, the public registry, or a repository that combines both. Without an explicit source rule, the package manager can choose a public package with the same name instead of the private package you intended to install.The execution path. The ecosystem and the package decide when the attacker’s code first runs. In npm, a package that includes a
postinstallscript runs that code duringnpm install, before anyone imports the package. A payload with no install script waits until the application first imports the package. In either case, the resolver selected the attacker’s package before its code executed.
The root failure is source selection. The build resolved a trusted name from an unapproved source, and nothing verified where the package came from before its code ran.
The origins of dependency confusion
Alex Birsan’s 2021 research brought dependency confusion to public attention. His original write-up, Dependency Confusion, started with a public PayPal package.json that referenced names that did not exist on the public npm registry.
After registering packages under those unclaimed names, Birsan added minimal callback code and waited for target environments to install them. Each installation ran that code inside the target environment, confirming that the public package had reached a private build system.
Each time one of Birsan’s packages was installed, it sent a signal back to him. He received those signals from more than 35 organizations using npm, PyPI, or RubyGems. Nearly three quarters came through npm because private JavaScript package names were easier to uncover in public website code, build files, and leaked source code.
Birsan’s research showed that an exposed internal package name can lead to code execution when a build is allowed to install that name from a public registry.
Internal package names leak through public code, bundles, logs, or documentation.
The attacker claims the name in a public package registry.
The attacker publishes a public package with the same name, often using a version that the resolver will prefer.
A developer machine, CI runner, or build system installs the public package.
Install-time or import-time code runs in the target environment.
Dependency confusion keeps recurring because builds install packages automatically. If an internal package name is not tied to a private registry, the build can install a public package with the same name before anyone checks where it came from.
How npm scopes route private packages to the right registry
An unscoped internal npm package is especially exposed when npm’s default registry is public. npm sends requests for unscoped packages to the configured default registry because the package name does not identify a private source. If the public registry contains a package with the same name, npm can install it instead of the private package your build intended to use.
A scope reduces this risk when it is mapped to the organization’s private registry. npm uses the scope at the start of a package name to decide which registry to query. When @company is mapped to your private registry, npm sends requests for @company/* there. Packages without a scope use the default registry, usually npmjs.com.
.npmrc is npm’s configuration file. The .npmrc example below configures npm to use your private registry for @company/* packages and the public registry for everything else:
@company:registry=https://packages.company.example/npm/
//packages.company.example/npm/:_authToken=${NPM_TOKEN}
registry=https://registry.npmjs.org/
npm sends package names that begin with @company/ only to your private registry. Names without that prefix use the public registry, where an attacker can claim a name such as auth-utils. The scope mapping tells the npm client which registry URL to query. It does not prevent that registry from proxying the request to a public registry.
In the May 2026 campaign, attackers made public packages look like internal company packages. They used company-looking scopes (the @company/ part of a package name) and familiar details in package.json to make the packages seem legitimate. Those scopes worked only where nothing mapped them to a private registry. When a scope is mapped in .npmrc, the npm client never queries the public registry for it. Version tactics varied by account. Attackers used version numbers either to make a public package more likely to be selected or to make it look like a legitimate release. When npm installed either package, it automatically ran the included postinstall script.
The torchtriton dependency confusion incident
In late December 2022, a dependency confusion attack affected PyTorch’s nightly Linux builds, which are pre-release packages published each day from the project’s latest code. PyTorch hosted the legitimate torchtriton package on its own nightly package index, while an attacker published a malicious package with the same name to PyPI, Python’s public package registry. The installation configuration let pip, the tool developers use to install Python packages, consider torchtriton packages from both sources. pip selected the malicious PyPI package instead of the package PyTorch intended users to install.
The malicious package was installed in environments running those nightly builds. The malicious package included an executable file that ran only when a user explicitly imported the triton module, rather than during installation. That executable read and uploaded sensitive files, including SSH keys under .ssh, .gitconfig, /etc/passwd, /etc/hosts, and the first 1,000 files in the user’s home directory.
The torchtriton incident shows the risk of configuring pip to search a private index and public PyPI for the same package name. The --extra-index-url flag tells it to check a second source in addition to the public PyPI registry. If both places contain internal-package, pip may install the public one instead of the private package. This is the unsafe setup pip warns about:
python -m pip install internal-package \
--extra-index-url <https://packages.company.example/simple>
That command tells pip to search both your private package source and the public PyPI registry for the same package name. A safer design routes Python installs through one approved index or repository proxy:
python -m pip install internal-package \
--index-url <https://packages.company.example/simple>
How GitHub Actions can introduce untrusted code
A GitHub Actions workflow automates tasks such as building and testing software. It can call reusable actions published outside your organization. If a workflow names the wrong action, uses a tag that later changes, or trusts an unverified publisher, the build can run code the team did not approve. In a workflow, uses: owner/action@ref tells GitHub to download and run reusable code, and its three parts identify who published the code, which action to use, and which version to run.
GitHub's secure use reference warns that a compromised third-party action can interact with jobs, access configured secrets, and use GITHUB_TOKEN depending on workflow permissions. GitHub recommends pinning actions to a full-length commit SHA, which identifies one exact snapshot of the action’s code. A tag such as v1 can later be changed to point to different code, but a commit SHA continues to point to the version your team reviewed.
Lookalike action names are typosquatting rather than dependency confusion, because the failure is a person referencing a lookalike name instead of a resolver choosing between two sources for one name. An attacker can create a misspelled variant of a trusted action such as actions/checkout. If a workflow uses that lookalike by mistake, GitHub downloads and runs the attacker’s code as part of the build. A 2025 study of 23,757 actions identified 25 typosquatting repositories, although the researchers could not establish malicious intent. Those repositories still created a risk because their owners could later change the code. A familiar-looking action reference can give untrusted code access to the workflow’s permissions and, in some cases, its configured secrets.
Package manifests and GitHub workflows both tell an automated build what outside code to use. A package manifest names libraries to download; a workflow names actions to run. Because either reference can bring untrusted code into the build, teams need clear rules about which package sources and action owners are allowed:
Pin third-party actions to full-length commit SHAs.
Restrict allowed action owners at the organization level.
Add workflow files to
CODEOWNERSand use a branch rule or ruleset that requires code-owner approval.Keep
GITHUB_TOKENpermissions read-only by default.For sensitive workflows, use actions from publishers whose identity GitHub has verified or company-controlled copies of approved actions. Publisher verification confirms identity, not code safety, so it does not replace pinning each action to a full commit SHA.
Three ways attackers get untrusted code into your build
The three tactics below all end with untrusted code in the build, but they work through different failures. Exploiting version precedence and scope confusion are dependency confusion in the strict sense, because the resolver selects the attacker’s package on its own. Namespace squatting is impersonation, and it needs a person to reference a lookalike name before any code runs. The same audits catch all three, so they belong in one review.
Tactic | How it works | What to audit |
Namespace squatting | The attacker registers a package, npm scope, GitHub organization, or action owner with a name that resembles one the company trusts. | Check public registries for internal package names, company abbreviations, and npm organization scopes that an attacker could claim. |
Exploiting version precedence | The attacker publishes a version high enough that a resolver considering both sources for the same name, or matching an open version range, selects it over the private version. | Configure each private package name so it can be downloaded only from its approved source. |
Scope confusion | An internal package is referenced without the company’s npm scope, or the scope is not mapped to the private registry. | Require internal npm packages to use the company scope, and fail the build when an internal package name omits it. |
Attackers also add fake company details to make malicious packages look internal. Microsoft’s May 2026 research found fabricated internal-registry, documentation, and Jira hostnames in package metadata, plus a fake telemetry disclaimer in the README. Those details do not affect which package the manager installs, but they can make a developer or reviewer more likely to trust the package and miss the warning signs.
A reviewer can miss a familiar-looking package or action during a busy release. Enforce approved package sources and GitHub Action owners automatically so the build rejects an unapproved package or action before its code runs.
Seven controls that reduce dependency risk
Reducing dependency risk starts by tying each internal package to one approved source. Each control below is labeled with the risk it reduces. Source-selection controls decide where a build may resolve each name from. Containment controls limit what a wrong install can change or run, and workflow hygiene applies the same source discipline to GitHub Actions.
Source selection. Register the package scopes and organization names your company actively uses, and monitor public registries for likely impersonations. PyPI does not offer organization scopes and may remove empty placeholder projects as name squatting, so use strict package-source rules for private Python dependencies.
Source selection. Enforce registry scoping for npm. Every internal JavaScript package should use an organization scope, and that scope should map to the private registry in
.npmrc. Fail builds that reference internal packages without the scope.Source selection. Stop using
--extra-index-urlfor private Python packages. Route installs through one approved package index or repository proxy. If a private package name can also be resolved from PyPI, change the configuration so pip can install it only from your approved package source.Source selection. Route installs through a policy-managed source. Give each ecosystem one approved endpoint that applies policy checks, such as cooldowns and malware scanning, before a dependency reaches a developer machine or build.
Containment. Lock versions and verify integrity. Commit
package-lock.json,npm-shrinkwrap.json, or the ecosystem’s equivalent. For Python, use a fully pinned requirements file with hashes. Lockfiles and hashes reproduce the package versions and contents chosen on the first install. They do not decide which registry supplied a private dependency. If that first install selected a malicious public package, later installs can reproduce the same choice. Review lockfile changes for unexpected package-source URLs or integrity-hash changes before merging them.Containment. Control lifecycle scripts. If the build does not need install scripts, use
npm ci --ignore-scripts. If it does, allow only approved packages to run them and require review when a newpreinstall,install, orpostinstallscript appears.Workflow hygiene. Pin workflow dependencies. Use full-length commit SHAs for third-party GitHub Actions. Restrict allowed action owners, add workflow files to
CODEOWNERS, and use a branch rule or ruleset that requires code-owner approval.
Source selection and artifact integrity
Most of the controls above reduce source-selection risk, the chance that a build resolves a dependency from an unapproved source. The lockfile and script controls limit how far a wrong install spreads and what it can run. Artifact integrity is a separate risk, because a correctly routed install can still deliver a package that was compromised before or during its build. Verified-source builds address that second risk with evidence that a selected package was built from the expected source through a controlled process. Neither control replaces the other. Policy rules decide where packages can come from, and build evidence lets teams verify what those sources delivered.
Chainguard Repository is a policy-managed source. Teams route npm install, pip install, and mvn install through one endpoint and set rules for every package that passes through it. When upstream fallback is enabled and Chainguard has not yet rebuilt a package, Repository retrieves the upstream version from the public registry and applies additional checks before distributing it. A configurable cooldown delays newly published versions and gives the wider ecosystem time to identify malicious releases, which blunts the inflated-version tactic used in the May 2026 campaign. Every package also passes through malware scanning before distribution.
Chainguard Libraries supplies the Python, JavaScript, and Java packages behind that endpoint. Packages that Chainguard rebuilds are produced from verifiable source in a SLSA Level 3-compliant factory, and each ships with signed provenance and a Software Bill of Materials (SBOM). Provenance records the source and build process behind the package, while the SBOM lists the components it contains. This evidence does not decide which source a build resolves from. It lets teams verify that the package their approved source delivered was built from the expected source through a controlled process.
Chainguard Actions provides hardened GitHub Actions that are continuously scanned for unsafe workflow configurations. Each action includes an SBOM and provenance. Teams should still pin each action to a full commit SHA and restrict workflow permissions. These measures help reduce the chance that an action changes after review, installs an untrusted dependency, or exposes secrets through an unsafe workflow setting.
How to evaluate your exposure to dependency confusion attacks
Start with one application. Review its package files, which list the software it installs, and its GitHub workflow files, which list the actions its automated builds run. For each internal package, record the only registry or repository the build may use. For each third-party action, record the approved owner and full commit SHA.
Turn those decisions into automated build checks. Fail the build when an internal npm package is missing its company scope, a private Python package can resolve from public PyPI, a new install script appears without approval, or a workflow calls an unapproved or unpinned action. These checks stop the build before it downloads a package from an unapproved source or runs an action the team has not reviewed.
Where signed provenance is available, verify it against the builder, source repository, and artifact digest you expect, and use the SBOM to review the components the dependency contains. These records support package review, but they should complement package-source rules rather than replace them.
Contact Chainguard to learn how a policy-managed package source reduces the chance that your builds resolve dependencies from unapproved sources, and how verified-source packages and hardened GitHub Actions give your team evidence to verify what those sources delivered.
Related articles