In February 2021, security researcher Alex Birsan published a technique that got code executing inside more than 35 companies — including Apple, Microsoft, PayPal, Shopify, Uber, Netflix, and Tesla — and he never exploited a single CVE. He simply found internal package names leaked in public package.json files, error messages, and JavaScript source maps, then published his own packages under those exact names on npm, PyPI, and RubyGems, tagged with inflated version numbers like 9.9.9. Build systems configured to check public registries pulled his packages automatically, because most dependency managers default to resolving whichever source offers the highest version number. Shopify, Apple, and PayPal each paid him roughly $30,000 in bug bounties; his total take exceeded $130,000. The technique came to be called dependency confusion, and it exposed a default-configuration blind spot that most companies still haven't fully closed five years later. This guide covers what actually stops it — scoped packages, registry priority, lockfiles with integrity hashes — and how it differs from the older, related threat of typosquatting.
What exactly did Birsan's dependency confusion attack exploit?
Birsan's attack exploited registry resolution order, not a bug. When a project depends on an internal package like @internal-corp/auth-lib that only exists on a private registry, but the build tool is configured to also check a public registry such as npmjs.org, most package managers historically had no concept of "this name should only ever come from the private source." If a public package existed with the identical name and a higher semantic version, npm, pip, and similar tools would fetch the public one — because default resolution logic is version-precedence-based, not source-trust-based. Birsan didn't need to guess names blindly; he mined publicly leaked internal manifests, GitHub commits, and client-side JavaScript bundles for real internal library names, then squatted on them publicly. The fix isn't a patch — it's changing how your tooling decides which registry to trust for a given name, which is exactly what scoping and registry pinning address.
How is typosquatting a different threat from dependency confusion?
Typosquatting relies on human or automated typing errors rather than resolution-order ambiguity — an attacker publishes crossenv hoping developers mean cross-env, or python3-dateutil hoping they mean dateutil. There's no internal package involved and no registry misconfiguration; the malicious package is simply sitting in the same public namespace as the legitimate one, waiting for a fat-fingered pip install or a copy-pasted README typo to pull it in. Security research firm Phylum has published repeated findings of large batches of malicious typosquats uploaded to PyPI in short windows, frequently targeting popular libraries with malicious setup.py install-time scripts that execute the moment the package is downloaded — before any application code even runs. Because typosquatting doesn't depend on a private/public naming collision, scoped packages and registry priority don't stop it. Defense requires name-similarity detection, allowlisting of approved dependencies, and inspecting packages before install — the same class of control Safeguard's Package Firewall applies at the point a package is fetched.
Do scoped packages actually close the dependency confusion gap?
Scoped packages close it for the specific attack Birsan demonstrated, because a scope like @your-org/ is a namespace that only your organization can publish under on a given registry — nobody else can register @your-org/auth-lib on the public npm registry even if they know the exact name. This converts an ambiguous unscoped name, which any registry could theoretically serve, into an unambiguous one tied to an owner. npm has supported scoped packages since 2015, and GitGuardian and Snyk both cite adopting scopes for all internally-authored packages as the first concrete step any npm-based organization should take after Birsan's disclosure. The catch: scoping only helps for the ecosystems that support namespacing well (npm scopes, Java/Maven group IDs). PyPI has no equivalent enforced namespace mechanism — which is why Python shops depend more heavily on registry priority configuration and internal package reservation than on naming alone.
Why don't lockfiles alone solve dependency confusion?
Lockfiles — package-lock.json, yarn.lock, poetry.lock — pin an exact resolved version and record an integrity hash for it, so a second npm ci or poetry install reproduces the same tree bit-for-bit instead of re-resolving against whatever the registry currently serves. That stops silent version drift after the first correct resolution. But a lockfile only protects you once it already contains the right package — if the very first resolution pulls the attacker's public package because your registry configuration prioritized it, the lockfile faithfully pins that malicious package forever, hash and all. Lockfiles are necessary for reproducibility and for detecting unexpected changes on future installs, but they're a second layer, not a substitute for correct registry priority. Treat lockfile diffs as a review gate too: a sudden appearance of an unfamiliar package, or a legitimate internal name suddenly pointing at a public-registry integrity hash instead of your private one, is a concrete signal worth blocking a merge over.
How does internal registry configuration actually prevent this?
The fix Birsan himself recommended, and that Sonatype and FOSSA both document as standard practice since 2021, is telling your package manager explicitly which registry owns which namespace instead of letting it check multiple sources by version precedence. For npm, that means setting scope-specific registries in .npmrc so @your-org:registry=https://your-private-registry routes every scoped lookup exclusively to your internal index, with no public fallback. For pip, it means configuring index-url to point only at your internal PyPI mirror (tools like Artifactory and Nexus proxy public packages through that same private index, so there's a single trusted source instead of two competing ones). A complementary tactic — reserving your real internal package names as empty placeholder packages on the public registries — closes the door even if a build is ever misconfigured to check public sources, since there's nothing for an attacker to squat on.
How Safeguard Helps
Safeguard's Package Firewall runs as an install-time proxy in front of npm and pip, so every fetch — including transitive dependencies pulled in by your direct dependencies — is evaluated before it reaches your machine or CI runner, with no separate lockfile-parsing step to keep in sync. It flags typosquats through look-alike name detection, and it specifically detects namespace and dependency-confusion patterns: names that could resolve to a public package in place of an intended internal one, or that squat on a namespace you own. You choose the enforcement posture — audit mode to baseline before turning on warn or block, or quarantine mode to hold a flagged package for review with automatic release once it clears (or aging out after 14 days if it never does). Every decision, allow, warn, block, or quarantine, lands in the audit trail with the package, the rule that fired, and the outcome, so a security team can prove — not just assume — that the resolution-order gap Birsan exploited in 2021 stays closed on every install, not just the ones someone remembers to check.