Dependency Confusion — Lock Registries and Pin Hashes
Dependency confusion swaps your private packages for public lookalikes.
20+ years shipping production backend systems. Everything here is grounded in real deployments.
- ✓A project with private packages plus public dependencies
- ✓Access to your registry or feed configuration
- ✓Basic familiarity with lockfiles in your ecosystem
- Dependency confusion happens when your build resolves a private package name from a public registry serving a lookalike version
- Attackers squat the names of internal packages on public registries, then wait for your installer to prefer the higher public version
- Point internal names at a private registry or virtual feed that always wins, and never let private names resolve publicly
- Commit lockfiles and pin artifact hashes so resolution results are frozen, reviewed, and reproducible on every machine
- Alert on any private-named package arriving from a public source — that event should never happen by design
Picture a company cafeteria that orders custom-labeled coffee from its own kitchen — but the purchasing clerk also accepts deliveries from any stranger at the loading dock. An outsider prints matching labels and drops off doctored bags, which the staff brews unquestioned. That's dependency confusion: your build orders internal packages yet accepts same-named substitutes from the public internet. The fix is a receiving policy: internal labels come only from the internal kitchen, seal verified.
Your build pulls hundreds of packages every day. Most come from public registries; a few are private libraries your team built — billing helpers, auth wrappers, shared UI kits. The installer resolves each name to a file, and for private names it checks your private feed first. Or does it? By default, most ecosystems check every configured source and pick the highest version number — including the public registry, where anyone can publish anything under names you haven't claimed.
That default is the dependency confusion hole. An attacker who learns (or guesses) your internal package names publishes lookalikes on the public registry with inflated version numbers. Your next build prefers the attacker's 99.0.0 over your internal 2.4.1, and their code runs with your build's privileges: reading secrets, exfiltrating source, poisoning releases. No phishing, no perimeter breach — just the installer doing exactly what it was configured to do.
This guide explains how private names leak into public view, how scoped registries and virtual feeds force private-first resolution, and why lockfiles with hash pinning turn resolution from a live gamble into a frozen, reviewable fact. Defensive configuration throughout — no attack tooling, just the receiving policy that keeps strangers out of your loading dock.
How Private Package Names Leak Into Public View
Attackers can't squat what they can't name, so the first half of dependency confusion is reconnaissance — and your organization is probably publishing its internal names in several places right now. Public repos with leftover private dependencies in package.json. Frontend bundles whose import paths name internal modules. Source maps that map minified code back to internal file trees. Build logs pasted into public issues. Conference slides with screenshots of directory listings. Each leak is small; together they hand over your namespace.
Guessing fills the gaps. Internal packages follow boring conventions — companyname-utils, platform-auth, shared-components — and attackers enumerate the obvious variants automatically. Short, generic names are the worst: they collide with real public packages and get claimed by opportunists regardless of targeting. If your internal name is also a plausible public name, assume it's already taken or about to be.
Treat name exposure as inevitable and design accordingly. Inventory every internal package name across ecosystems, claim each on the corresponding public registry as a reserved placeholder (most registries support this), and monitor for new claims matching your patterns. Reservation is cheap insurance — it removes the attacker's foothold — but it's the outer fence, not the wall. Resolution controls decide what your builds actually install, and those come next.
Scoped Registries and Virtual Feeds That Prefer Private
The core fix is making resolution deterministic: internal names resolve from your private feed, always, regardless of what the public registry offers. npm scopes are the cleanest mechanism — map @acme/* to your registry in .npmrc and no public package can shadow those names, because scoped resolution never consults the public registry for your scope. Python's equivalent is stricter index configuration: a private index as the primary source, ideally behind a proxy that serves private packages first and fetches the rest upstream.
Virtual feeds (repository managers like Artifactory, Nexus, or cloud artifact registries) generalize this across ecosystems: one internal endpoint that checks private storage first and proxies public registries second. Builds point only at the virtual feed and never at the public internet directly. The ordering inside the feed is the security policy — private-first is the only safe mode — so review it like firewall rules, not like a performance tweak.
Verify, don't trust. After configuring, run a resolution dry-run and confirm each internal name maps to your feed's URL. The .npmrc snippet below shows the shape for npm: scope pinned to the private registry, public registry explicit for the rest, scripts disabled in CI. Repeat the exercise per ecosystem — pip, Maven, NuGet each have their own knobs, and an attacker only needs the one you forgot.
Lockfiles and Hash Pinning That Freeze Resolution
Resolution controls decide where names come from; lockfiles decide what exactly gets installed. A lockfile records the resolved version, source URL, and integrity hash of every package in the tree — including transitive dependencies you'd never name directly. Committed and enforced, it converts each build from a live negotiation with registries into a replay of reviewed decisions. An attacker's 99.0.0 can't win if the build never asks who's newest.
Enforcement is what separates protection from paperwork. Commit the lockfile, install with --frozen-lockfile (or your ecosystem's equivalent) in CI, and fail the build on any deviation. Require integrity hashes on every entry so a compromised mirror serving altered tarballs under correct versions still fails verification. Review lockfile diffs in pull requests with fresh eyes: source-registry changes, version jumps that skip internal numbering, and new transitive dependencies are the three signals that deserve a security look before merge.
Watch the escape hatches. Lockfile regeneration flags (--force, update-all) re-run resolution and can bless a substitution as the new truth; restrict them to explicit dependency-update workflows with mandatory review. The package.json snippet below shows the npm side: exact integrity metadata that makes tampering detectable. Pair it with a CI check that fails on registry-source changes for internal names, and confusion attempts become build failures instead of incidents.
Namespaces, Scopes, and Reserved Names
Beyond your own feed, use every ownership mechanism your ecosystem offers. npm scopes and organization namespaces, PyPI project ownership, Maven groupIds under domains you control, NuGet reserved prefixes — each of these converts a name from first-come-first-served into yours-by-right. Register them early, including the variants (acme, acme-inc, acmeinc) that squatters reach for when the exact name is taken.
Publish reserved placeholders for internal names on public registries: a minimal package whose only job is occupying the name with a clear 'private to Acme, do not use' readme. This isn't security by itself — placeholders can be Deprecation-ignored by determined attackers only if your resolution is broken, and with proper scoping they can never win — but it closes the casual-squat window and signals ownership to anyone who stumbles on the name.
Document the namespace policy where developers will actually read it: the new-service template, the package-publishing runbook, the contractor onboarding checklist. Every new internal library should be born scoped, reserved, and feed-pinned — never migrated later, because 'later' is where the incident in this article lived. A ten-minute naming checklist at creation beats a four-hour credential rotation after publication.
CI Checks That Catch Substitution Before Deploy
Configuration rots — feeds get reordered during migrations, scopes get dropped in refactors, lockfiles get force-regenerated at midnight. CI checks are the ratchet that holds your posture between audits. Build three gates: resolution assertion (every internal name resolves to your feed), hash verification (installed artifacts match the lockfile), and diff review triggers (registry-source or major-version changes require security sign-off).
Make the gates loud and specific. A failure should name the package, the expected source, and the unexpected one — 'billing-utils resolved to public registry, expected artifacts.example.com' — so any developer can act without a security background. Page on private-names-from-public-sources; that's not a flaky test, it's an active attack or a broken control, and both deserve immediacy.
The pipeline snippet below shows the resolution gate in portable form: extract each internal package's resolved URL from the lockfile and assert it belongs to your feed. Ten lines, runs in seconds, and it's the check that would have stopped the incident above at the pull-request stage — before 60 agents executed anything. Add the companion monitor outside CI: watch public registries for new claims on your names so you're warned even before a build runs.
Responding When a Confused Dependency Ships
If a substitution already executed, you're in incident response, not configuration review. Assume the payload ran with full CI privileges: revoke and rotate every secret visible to the build environment — cloud credentials, registry tokens, signing keys — because install scripts exfiltrate first and ask never. Quarantine affected agents and rebuild from clean images rather than 'cleaning' them; persistence through build caches and shared volumes is the norm, not the exception.
Scope with artifacts, not assumptions. Identify every build that resolved the malicious version (lockfile history and feed access logs), every artifact those builds produced, and every environment those artifacts reached. Redeploy from known-good builds, don't patch forward from tainted ones. Preserve the malicious package and full logs for analysis before purging — you need its behavior (what it stole, where it sent it) to scope notifications and regulatory obligations.
Then fix the system, not just the symptom. The incident above ended with scoping, placeholders, private-first feeds, frozen lockfiles, and script restrictions — the full stack from this guide. Write the retrospective around the question that matters: which layer should have caught this, and why didn't it? A substitution that ships is never one missing control; it's the gap between controls you thought overlapped.
A Squatted Internal Name Ran Code on 60 Build Agents in 4 Hours
- Unclaimed internal names are public attack surface: squat your own names on public registries before strangers do.
- Highest-version-wins across mixed sources is the vulnerability. Make private feeds authoritative for internal names instead of merely additional.
- Lockfile diffs are supply-chain reviews: a version that can't exist internally should fail the build, not await a sharp-eyed reviewer.
| File | Command / Code | Purpose |
|---|---|---|
| for name in billing-utils platform-auth shared-components; do | How Private Package Names Leak Into Public View | |
| .npmrc (committed at repo root) | @acme:registry=https://artifacts.example.com/npm-internal/ | Scoped Registries and Virtual Feeds That Prefer Private |
| package-lock.json (excerpt — committed) | { | Lockfiles and Hash Pinning That Freeze Resolution |
| FEED='artifacts.example.com/npm-internal' | CI Checks That Catch Substitution Before Deploy |
Key takeaways
Common mistakes to avoid
5 patternsAdding the private feed as a fallback behind the public registry
Assuming internal names are secret
Regenerating lockfiles in automated jobs without review
Running install scripts with full CI privileges
Gating only release branches with supply-chain checks
Interview Questions on This Topic
What is dependency confusion in one paragraph?
Frequently Asked Questions
20+ years shipping production backend systems. Everything here is grounded in real deployments.
That's Supply Chain. Mark it forged?
5 min read · try the examples if you haven't