Dependency Confusion and Typosquatting: Practical Defense for Dev Teams
How registry policies, lockfiles and scoping block malicious packages before they hit the build. Hands-on technical guide from the Basilisk team.

In this article
Back in 2021 a researcher earned over 130 thousand dollars in bug bounties by publishing packages with internal names from Apple, Microsoft, PayPal and dozens of other companies on public npm and PyPI. The attack used zero zero-days: it simply exploited the precedence package managers give to the public registry when a name exists on both sides. Five years later, Basilisk teams still find this vector wide open in seven out of ten audited pipelines. Dependency confusion and typosquatting are not academic curiosities, they are the cheap front door to compromise an entire build and, by extension, every customer who receives that artifact. This post breaks down both vectors, walks the attack chain step by step, and hands you a defense you can commit to your CI today.
What dependency confusion actually means#
The mechanism is simple and brutal. You have an internal package called acme-payments-sdk hosted on a private Nexus or Artifactory. An attacker discovers that name in a leaked package.json, a Stack Overflow post or a public docker layer, and publishes acme-payments-sdk@99.0.0 on npmjs.com. Next time the pipeline runs npm install without a strict lockfile, the resolver picks the higher version, because the default config still includes the public registry and SemVer prefers the largest compatible release. Done, arbitrary code running inside your CI with AWS credentials, GitHub tokens and access to the internal registry. We saw this in the field during assessments similar to REST and GraphQL API Pentest: Technical Checklist for Legal Bug Bounty, where the build surface was more exploitable than the API itself. The root cause is that npm, pip and others share a flat namespace for internal and public and, without explicit config, demand no proof of origin.
Typosquatting as a distinct vector#
Typosquatting plays in another league. Instead of taking over the real name, the attacker registers colorss, requets, python-dateutill, lodahs or djanga. The payload sits in a postinstall hook or straight in the imported module and runs the moment someone fat-fingers a name or an AI assistant suggests a hallucinated package (the so-called slopsquatting effect). Phylum research in 2024 cataloged more than eleven thousand malicious packages on PyPI using this pattern in twelve months. Payloads vary: env variable theft, mining, RAT installation, DNS exfiltration of ~/.aws/credentials. The first defense is cultural: mandatory code review on any dependency change, no blind version bumps. Tools like Socket, Snyk Advisor and deps.dev already flag freshly published packages, packages without maintainer history or with suspicious install script patterns, giving you a risk signature before the merge.
The attack chain step by step#
Understand the attacker and you understand the defense. Phase one is reconnaissance: the attacker harvests internal package names from GitHub searches for @acme, from leaked .npmrc files, from frontend bundle sourcemaps or from error messages in public CI logs. Phase two is publication: they register the same package with an absurdly high version on the public registry and embed a preinstall hook that hits a callback URL. Phase three is detonation: your CI pulls the public package on any build, and the hook runs with the runner's privileges. Phase four is persistence and exfiltration: the code reads environment variables, OIDC tokens and deploy keys and ships them out, often over DNS or an innocent-looking HTTPS POST. This is exactly where supply chain compromise merges with classic post-exploitation, which is why the same mindset as incident response applies.
Lockfiles and hash pinning#
Lockfiles solve eighty percent of the problem if you use them seriously. package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, Cargo.lock and go.sum pin not just the version but the integrity hash. Configure npm ci, pnpm install --frozen-lockfile, pip install --require-hashes and cargo --locked in pipelines. No loose npm install in production, because that is allowed to mutate the lockfile. Combined with an .npmrc setting registry=https://nexus.internal/repository/npm-group/ and always-auth=true, you drastically reduce the chance the resolver wanders to the public registry by accident. Crucially, a lockfile only protects you if the CI runner respects it and does not regenerate it. The mental pattern echoes what we covered in Supply Chain Security: Sigstore Signing and Real SBOMs in CI/CD about SBOMs and attestations.
Scoping and private registries per ecosystem#
Scoping is the definitive weapon against dependency confusion in JavaScript. Migrate every internal package to @acme/payments-sdk, @acme/auth, @acme/billing. In .npmrc, set @acme:registry=https://nexus.internal/. Now npm only resolves that scope on the internal registry, even if someone publishes @acme/payments-sdk on npmjs (Microsoft has been reserving common scopes since 2021). In Python, use separate indexes: pip --index-url for internal and --extra-index-url only when truly needed, because both indexes get merged and the higher version wins. Better, mirror everything via devpi or Artifactory and set only index-url. For Go, GOPRIVATE=git.acme.com plus GONOSUMCHECK discipline blocks queries to proxy.golang.org. This hardening connects directly to Linux Server Hardening: Applying CIS Benchmark Without Breaking Production on the least privilege principle.
CI isolation and ephemeral credentials#
On the CI side, isolate ruthlessly. Every build job should run with ephemeral tokens, no production access, network egress filtered by a proxy allowing only the internal registry, github.com and known endpoints. OIDC with short-lived credentials on GitHub Actions or GitLab CI removes the long-lived secrets an install hook could steal. Disable install scripts where possible with npm ci --ignore-scripts and allow them only for a curated allowlist. Run the build in a non-root container, read-only filesystem, no access to the docker socket. That way even a successful install hook becomes a blunt knife: it finds no long-lived secrets, cannot phone home, and does not survive the end of the job. This segmentation is cheaper than any incident and also protects the case where a legitimate dependency is later hijacked upstream.
Verification before installation#
Add a pre-install verification step: scripts/check-deps.sh runs npm audit signatures, validates that no dependency was published in the last seven days without manual approval (a cooldown against fresh hijacks), and checks packages against an allowlist. Layer in osv-scanner against the OSV database and Socket in CI, which shows behavioral diffs between versions: new network calls, new filesystem access, newly added install scripts. A package that suddenly imports child_process or contacts an IP gets blocked and goes to manual review. Datadog published a 2025 report showing sixty-two percent of supply chain compromises would have been blocked by these three rules combined. No single tool is a silver bullet, but chaining cooldown, signature checks and behavioral diff closes the most common paths.
Continuous monitoring and name protection#
Continuous monitoring closes the loop. Configure Sigstore Rekor alerts for any publication carrying your organization name, defensively register the typo-squatting domains and package names of your product (acme-cloud, acme-coud, acmecloud), and keep an inventory of cosign-signed SBOMs for every release. When a suspicious package shows up you already have evidence for takedown and data for incident response, in the style we showed in DFIR on Linux: Live Triage with UAC and Velociraptor. Proactively reserve your internal package names as empty placeholders on the public registry so nobody can squat them. Train the team to report any new install script warning instead of reflexively ignoring it, because that warning is often the only signal before compromise.
Common mistakes in the wild#
Five pitfalls show up in nearly every audit. First: --extra-index-url as a Python default, which blends internal and PyPI and throws the vector wide open. Second: lockfiles that get regenerated instead of respected in CI, because someone left npm install instead of npm ci in the script. Third: internal packages with no scope, so any public namespace can shadow them. Fourth: install scripts allowed globally even though ninety percent of builds do not need them. Fifth: long-lived npm or PyPI tokens in CI that a hook exfiltrates and that stay valid for months. Each of these alone is enough for a compromise; together they are an open barn door. The good news is that each can be fixed in under a day and none requires a code change to the product itself.
Practical checklist#
Work this list today. One: migrate all internal npm packages to an @scope bound to the internal registry. Two: enforce npm ci, --frozen-lockfile, --require-hashes or --locked in every pipeline. Three: in Python use only index-url pointing at the internal mirror, no --extra-index-url. Four: set GOPRIVATE for all internal Go modules. Five: disable install scripts by default, maintain an allowlist. Six: OIDC instead of long-lived registry tokens. Seven: an egress proxy in CI with an allowlist. Eight: osv-scanner, npm audit signatures and Socket as a merge gate. Nine: defensively reserve internal names on the public registry. Ten: Rekor alerts on the organization name. Tick these ten and you have bolted the cheap front door shut.
FAQ#
Is a lockfile alone enough against dependency confusion? No. A lockfile protects existing dependencies, but the moment you add a new internal dependency or regenerate the lockfile, registry precedence bites again. Binding the scope to the internal registry and an egress proxy are the structural fix; the lockfile is the second layer.
Is typosquatting only an npm and PyPI problem? No. RubyGems, Maven Central, NuGet, Crates and even Docker Hub images are affected. The defensive approach is the same everywhere: a curated internal mirror, behavioral diff between versions, a cooldown for freshly published artifacts and human review on every dependency change.
Conclusion#
Dependency confusion and typosquatting are not exotic attacks, they are the cheapest way to walk past an organization without touching a single line of its actual code. The defense is well known, cheap and doable today: scope binding, hash pinning, isolated CI with ephemeral credentials, verification before installation, and continuous monitoring. Practical takeaway: today, audit your .npmrc, .pip.conf and go env. If you cannot answer in thirty seconds which registry each dependency comes from, you are already vulnerable. Attackers do not need a zero-day; they are just waiting for someone to leave the registry order unconfigured.


