Alex Birsan published the original dependency confusion write-up in February 2021 and collected bug bounties from Apple, Microsoft, PayPal, Shopify, and 31 other companies inside a week. Five years later, the same attack class still lands in 2026 — not because the controls are unknown but because teams keep deploying partial versions of them. Security Week's tracking of 2025 package registry attacks recorded over 1,400 dependency confusion or typosquat incidents across npm, PyPI, and RubyGems, with notable ones targeting Amazon AWS internal tooling in February 2025 and a wave of fake @azure/* scope squats in June. The attack keeps working because package managers default to trust-on-first-use, because internal packages get shared names with public ones, and because most teams never audit their install logs.
What is dependency confusion, exactly?
Dependency confusion is an attack that exploits how package managers resolve names when the same package identifier exists in both an internal private registry and a public one. When a developer's tool is configured to check multiple registries — or when the configuration is ambiguous — the package manager may pick the public version if it has a higher version number or if the public registry is checked first. An attacker who learns the name of an internal package (by reading a leaked package.json, inspecting a webpack bundle, or scraping a CI log) can publish a package with the same name to the public registry, bump the version, and wait for the victim's build system to pull their malicious copy.
Birsan's 2021 proof-of-concept used this pattern to execute arbitrary code inside Apple, Microsoft, and PayPal build systems. The mechanics haven't changed. What has changed is the sophistication of the payloads — where 2021 attacks mostly beaconed out a DNS query, 2025 attacks routinely include post-install hooks that enumerate cloud credentials, exfiltrate repository contents, and plant persistent access in CI runners. The Birsan-style identification phase now runs at scale; researchers at Aqua Nautilus documented over 90,000 auto-generated confusion attempts on npm alone in the first half of 2025.
Why does dependency confusion still happen in 2026?
Dependency confusion still happens because the three controls that block it — scoped package names, explicit registry pinning, and lockfile verification — each require organizational discipline and none are on by default. npm introduced scoped packages (@mycompany/foo) in 2014, but teams that started with unscoped internal packages and never migrated are still exposed. pip's index-url can be explicitly pinned to an internal registry, but if a developer runs pip install in a new environment without the pip.conf, they hit PyPI. Maven's <mirrorOf> is correct by default for most uses, but when teams add *,!internal patterns they often invert the logic unintentionally.
The second reason is that internal packages leak. Every CI log that prints npm install output, every frontend bundle that includes source maps, every error message that reports a missing internal module telegraphs package names to anyone who cares to look. Package names are not secrets, and treating them as such is a failed control. The only working controls assume the name is public and prevent confusion even when the attacker knows exactly what to publish.
How do you block dependency confusion in npm?
Use scoped packages for every internal package, publish the scope to your internal registry, and pin the scope to that registry in an .npmrc that's committed to the repository and enforced in CI. A working .npmrc looks like this:
@mycompany:registry=https://npm.internal.example.com/
//npm.internal.example.com/:_authToken=${NPM_INTERNAL_TOKEN}
registry=https://registry.npmjs.org/
always-auth=true
That configuration tells npm: any package under the @mycompany scope resolves from the internal registry, everything else from the public registry. An attacker who publishes @mycompany/utils to the public npm registry cannot win the resolution because the scope-level mapping overrides the default registry. The critical bit is that the .npmrc lives in the repository, not in developers' home directories, so a fresh clone plus npm ci cannot fall back to a misconfigured default.
Pair that with npm audit signatures, which verifies provenance for any package that publishes it (about 40% of top-1000 packages as of early 2026), and with a Sigstore-verified lockfile. Tools like lockfile-lint and socket.dev's CI integration detect when a lockfile suddenly resolves to an unfamiliar registry URL — a classic indicator that confusion has landed. Configure your CI to fail when package-lock.json diffs include a change in resolution URL for an internal scope.
What does the equivalent look like for pip and PyPI?
For pip, set index-url to your internal registry and use extra-index-url only for packages you know are exclusively public, because pip's default behavior is to pick the highest version across all configured indexes. That default is the root of most pip dependency confusion incidents. The cleanest configuration uses an internal registry that proxies PyPI:
# pip.conf committed to the repo root or CI config
[global]
index-url = https://pypi.internal.example.com/simple/
# Do NOT add --extra-index-url https://pypi.org/simple/
Combined with a proxying registry like JFrog Artifactory, Sonatype Nexus, or devpi, this routes every resolution through a single index that applies allow-listing, typosquat detection, and version pinning. An attacker who publishes my-internal-package==99.9.9 to public PyPI cannot hijack the resolution because pip never queries public PyPI directly; the proxy registry checks its internal mirror first and only falls through to the public source for explicitly allowed names.
PEP 708, which shipped in 2024 and is implemented in pip 24.2+, adds registry-side metadata that lets an index indicate which projects it considers authoritative. That closes the remaining gap for teams that must use extra-index-url, because pip can be told to reject a package when the wrong index claims authority. For Python specifically, also enable hash checking in your requirements files — requirements.txt entries with --hash=sha256:... fail the install if the resolved artifact doesn't match, which defeats last-minute tampering at the registry level.
How do you detect typosquats before they cause damage?
Detect typosquats by running every new or updated dependency through a name-similarity check against your existing lockfile and against known-popular package names, and flag matches below an edit-distance threshold. Typosquat packages like reqeusts (swapped letters from requests), python-colorlog (lookalike of colorlog), or @types/axios-http (plausible but fake type definitions for axios) rely on a developer misreading a character. Automated checks catch them reliably.
Tools that do this well in 2026 include OpenSSF Scorecard's name-similarity checks, the npm @lavamoat/allow-scripts tooling, Socket's CI integration, Snyk's Vulnerability Database typosquat flags, and the dep-scan project from AppThreat. The concrete recipe is: during PR review, diff the lockfile, extract added or upgraded packages, run Levenshtein or Jaro-Winkler similarity against the top 10,000 packages on the relevant registry, and fail the build or require explicit review when a new package is within edit distance 2 of a popular one but isn't itself popular.
Beyond name similarity, check for package-metadata anomalies: a first-time publisher with no download history, a package published within the last 24 hours, a maintainer whose email was changed in the last week, or a sudden version jump (e.g., 1.0.0 to 99.9.9) that indicates someone is trying to win resolution against an internal version. Any one of these is suspicious; two in combination should block the install.
Which detections actually work in CI pipelines?
The detections that work in CI are lockfile diff analysis, resolution URL pinning, and signed provenance verification; the ones that don't work are retrospective scans of installed packages and blocklists of known-bad names. Retrospective scans always lose the race because the attack has already executed post-install hooks by the time you scan. Blocklists are reactive by definition.
A working CI configuration does three things at PR time. First, it runs npm ci --ignore-scripts (or the pip equivalent, pip install --no-deps --require-hashes -r requirements.txt) to fetch artifacts without executing install hooks, so post-install payloads don't run against your CI runner. Second, it diffs the lockfile and refuses any resolution URL change for scoped internal packages, which would indicate that the scope-registry mapping was bypassed. Third, it verifies Sigstore provenance where available and records the attestation alongside the build output, so any downstream verification has the chain of evidence intact. Tools like socket-cli, npq, and dep-scan integrate cleanly with GitHub Actions and GitLab CI, and all three support failing the build on policy violations rather than just emitting warnings.
How Safeguard.sh Helps
Safeguard.sh prevents dependency confusion by analyzing lockfile changes in context — not as isolated version bumps but as shifts in the trust graph. Our SBOM generation and ingestion module records every resolution URL, every internal vs. public scope mapping, and every publisher identity for packages entering your build, so a new dependency pulling from an unexpected registry stands out immediately. Reachability analysis cuts 60-80% of CVE noise and also lets you reason about which confused packages would actually execute in your application, so the severity of a suspected incident is grounded in whether the trojaned code reaches a live entry point. Griffin AI autonomous remediation responds by pinning the internal version, opening a PR to re-scope the package, and updating the .npmrc or pip.conf to close the confusion window — typically within minutes of detection. The TPRM module extends the same analysis to vendor SBOMs, so a partner shipping software built against confused dependencies is flagged before you ingest their artifacts. With 100-level dependency depth scanning and container self-healing, the window between publication and exploitation shrinks from days to single-digit minutes.