UnveilTech

UnveilScan Blog

← All articles

Try UnveilScan free

Dependency confusion: npm and PyPI in 2026

Posted 2026-04-29 · 7 min read · supply-chainadvanced

In 2021 Alex Birsan published bug bounty results for a technique he called dependency confusion: registering public packages with the same names as a target's internal packages, with a higher version number. Several Fortune 100 companies' build systems pulled the malicious public packages instead of the legitimate internal ones. Five years later we still see the same misconfigurations during scans.

The mechanic

Internal package registries (Artifactory, Nexus, GitHub Packages, custom proxies) typically operate as a "fall-through" cache: ask for my-internal-utils, the registry checks its internal index first, falls back to the public registry if not found. The vulnerability: many tools (older npm, pip with --index-url plus --extra-index-url) check both registries and pick the highest version they find, regardless of source.

Attacker workflow:

  1. Find an internal package name. Sources: leaked package.json in a GitHub repo, source maps in production JS, an open S3 bucket, an old job description listing internal libraries.
  2. Register that name on npmjs.com (or PyPI, RubyGems, etc.) with version 99.99.99.
  3. Wait. The next CI run that resolves the package picks up the public version because 99.99.99 > 1.4.7.
  4. Malicious package's postinstall hook runs in the CI runner with whatever credentials are in the environment.

What we still see in 2026

During Extended scans, our checkers surface several signals correlated with dependency confusion exposure:

These don't prove a vulnerability — they tell an attacker which package names to register on the public registry.

npm: scoped packages are the answer

Use @your-org/ scoped packages and configure the scope to point to your internal registry exclusively:

# .npmrc (committed to repo root)
@your-org:registry=https://npm.internal.example.com/
//npm.internal.example.com/:_authToken=${INTERNAL_NPM_TOKEN}
registry=https://registry.npmjs.org/

Now @your-org/anything resolves only from the internal registry. Even if an attacker registers @your-org/anything on npmjs.com (which they can't because the scope is owned by your account anyway), npm doesn't look there for scoped packages.

Critically: own the scope on the public registry. Register @your-org on npmjs.com even if you don't publish anything there. If you don't, an attacker can register the scope and squat your namespace.

PyPI: there's no scope concept

Python packaging has no scoping. pip install internal-utils will look anywhere your indexes are configured. The mitigations:

Other ecosystems

EcosystemHas namespacing?Mitigation
Maven Central (Java)Yes (groupId)Own your com.example.* groupId on Sonatype OSSRH
NuGet (.NET)Prefix-reservationReserve your YourCompany.* prefix
RubyGemsNoSame as PyPI — squat names + single index
Cargo (Rust)No (until 2025 namespaces RFC)Squat names + private registry
Go modulesYes (import path = host)Inherent: go.example.com/internal can only come from your VCS

Go's import-path-as-URL is the only ecosystem where this is structurally hard. Unfortunately Go made other supply-chain choices (no signing, vendored proxies) that create different problems.

Postinstall lockdown

Even with namespacing right, malicious legitimate-namespace packages exist (event-stream 2018, ua-parser-js 2021, dozens of shorter incidents). A defense-in-depth: disable postinstall hooks in CI:

# For npm
npm install --ignore-scripts

# Or globally for the org via .npmrc
ignore-scripts=true

Some packages legitimately need postinstall (native compilation, asset download). Solve this by maintaining an allowlist of packages whose postinstall is trusted. Snyk's "can-i-ignore-scripts" tool does the heavy lifting.

Sigstore is finally landing

In 2026 Sigstore-based signed publishing is finally rolling out across npm and PyPI. Packages signed by their maintainers via OIDC (workflow identity in GitHub Actions, no long-lived keys). Verifiers can check the signature came from the expected repo+workflow. This makes typosquats and account-takeover attacks much harder. Adoption is still early — assume mostly unsigned for now.

Find what your prod JS leaks

Free Basic scan reports source map exposure. Extended adds Next.js data inspection, lock-file probes, version-disclosing JS lib detection.

Run a scan