Best Practices

How to Prevent Dependency Confusion in npm (2026)

Dependency confusion attacks are still landing in 2026 because scoped packages, registry config, and provenance checks are misconfigured by default. Here is the fix.

Shadab Khan
Security Engineer
7 min read

Alex Birsan published the original dependency confusion writeup in 2021 and it has been low-effort, high-impact for attackers ever since. The technique is embarrassingly simple: if a private package named @acme/internal-auth lives only on your internal registry, an attacker publishes a package with the same name on public npm with a higher version number, and your CI happily pulls the attacker's code because the default resolution rules prefer higher versions.

Five years on, the default is still broken. Scoped packages on npmjs.org are not automatically reserved. Many teams still have .npmrc files that check public npm first. The provenance ecosystem has matured but most projects do not verify it. This post is the 2026 playbook that actually closes the door.

What Is Dependency Confusion, Exactly?

Dependency confusion is a package-resolution attack where an attacker publishes a malicious package to a public registry using the same name as a private internal package, exploiting tooling that falls back to the public registry when it cannot find the package privately or prefers the higher version it finds publicly. It works against npm, PyPI, RubyGems, and any ecosystem with similar resolution semantics. The fix is the same everywhere: make the resolution deterministic.

Why Does This Still Happen in 2026?

Teams generally fail at one or more of these four defenses: they have not registered their scope on public npm, their .npmrc is not locked to the private registry for the right scope, their lockfile is not enforced at install time, or their CI accepts unverified provenance. Attackers do not need all four doors to be open — one is enough.

Step-by-Step Implementation

Step 1: Reserve Your Scope on Public npm

Register the organization scope used by your private packages on npmjs.org even if you never plan to publish there. Reserving @acme on public npm means an attacker cannot create @acme/anything and feed it to you. Create a dedicated npm organization (not a personal account) and add 2FA-enforcing admins. Do not skip this step — it is free and it blocks the simplest attack variant.

Step 2: Scope-Lock Your .npmrc

Your root .npmrc must pin every internal scope to the internal registry explicitly. Do not rely on registry= alone — per-scope config overrides the global default, and that is what you want:

# .npmrc at repo root
@acme:registry=https://npm.internal.acme.com
@acme-internal:registry=https://npm.internal.acme.com

# public packages go to public npm
registry=https://registry.npmjs.org

# require auth for internal registry
//npm.internal.acme.com/:_authToken=${NPM_INTERNAL_TOKEN}

# fail installs if lockfile is out of date
engine-strict=true

Commit this file. Do not rely on developer machines having a correct global .npmrc.

Step 3: Use overrides and resolutions to Pin Critical Paths

For any dependency that carries elevated trust — auth libraries, crypto, CI-only tooling — pin both the version and the registry. Yarn and npm both support explicit resolution overrides:

{
  "name": "acme-app",
  "overrides": {
    "@acme/internal-auth": {
      ".": "1.4.2"
    }
  }
}

This prevents a transitive dependency from dragging in a different @acme/internal-auth than the one you expect.

Step 4: Enforce Lockfile Integrity in CI

Use npm ci, not npm install, in CI. npm ci fails the build if package.json and package-lock.json are out of sync and does not update the lockfile. Combine it with --ignore-scripts unless you genuinely need postinstall scripts — most dependency confusion payloads are delivered via postinstall:

npm ci --ignore-scripts

For projects that require scripts, audit every package that declares one and enforce an allowlist via can-i-ignore-scripts or a custom check.

Step 5: Verify npm Provenance on Trusted Packages

Packages published with npm publish --provenance from a GitHub Actions workflow include a Sigstore-signed attestation linking the package back to the source repo and build. Verify it during install for your critical dependencies:

npm audit signatures --registry=https://registry.npmjs.org

In CI, wrap this in a blocking check. If npm audit signatures reports unverified or missing provenance for a package on your allowlist, fail the build.

Step 6: Use a Proxy Registry With Allowlist and Hold Policies

Stand up Verdaccio, Artifactory, or Nexus as a proxying registry in front of public npm. Configure two policies: an allowlist of scopes and packages that can be proxied from public npm at all, and a mandatory hold period (72 hours) for any new package version before CI can pull it. The hold window is the single most effective control against zero-day malicious packages — it gives the npm security team and community time to flag and unpublish.

# Verdaccio config.yaml
storage: /verdaccio/storage
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    agent_options:
      keepAlive: true
packages:
  '@acme/*':
    access: $authenticated
    publish: $authenticated
    proxy: []
  '@types/*':
    access: $authenticated
    proxy: npmjs
    min_age_hours: 72
  '**':
    access: $authenticated
    proxy: npmjs
    min_age_hours: 72

Step 7: Monitor Public Registries for Your Package Names

Subscribe to registry notification feeds (npm-replicate stream, socket.dev's watcher, or the Safeguard.sh takedown monitor) for any package name that matches your internal scopes or looks like a typosquat of your organization's name. Treat any match as a P1 until proven benign.

What About npm install Behavior on Developer Machines?

Developer machines need the same protections as CI. Ship a repo-level .npmrc and a preinstall script that fails if the developer's global config could override it:

{
  "scripts": {
    "preinstall": "node ./scripts/verify-npmrc.js"
  }
}

The verify-npmrc.js script can spawn npm config get @acme:registry and fail if the result is not your internal registry.

How Do You Handle Typosquats of Your Scope?

Scope registration handles the exact-match case. For typosquats (@acmes/, @acme-inc/, @acne/), automate detection with a nightly job that queries the public npm registry for any package whose name is within an edit-distance of 2 from your scope or top-level package names. File takedown requests via npm support for clear typosquats. The npm security team is responsive but they need you to report.

What If a Malicious Package Slips Through Despite All This?

Assume it will happen once and plan for it. Your incident runbook should include: identifying the affected lockfiles across every repo, forcing a clean node_modules rebuild in every environment, rotating any secrets that lived on developer machines or CI runners that ran the package, and scanning artifacts built during the exposure window for exfiltrated material. The worst-case mean time to recovery gets shorter the more of this is scripted.

Does This Apply to Yarn, pnpm, and Bun?

Yes — the attack class is the same, the controls are the same, and the config syntax differs. Yarn Berry uses .yarnrc.yml with npmScopes: blocks. pnpm uses .npmrc the same way npm does, plus pnpm-specific public-hoist-pattern to contain hoisting surprises. Bun uses bunfig.toml. Regardless of the tool, the principle is: every internal scope must be pinned to an internal registry and every install must fail closed when provenance or lockfile checks fail.

How Safeguard.sh Helps

Safeguard.sh watches public npm continuously for names that collide with your registered private scopes and for typosquats of your organization's top-level packages, with Griffin AI triaging every match to cut false positives. Our reachability engine identifies which packages in your lockfiles actually get executed versus which are dev-only noise, letting you prioritize hold-period and provenance-verification rules where they matter. The TPRM module tracks whether your upstream vendors have taken these same controls, and the container self-healing runtime rebuilds images and rotates tokens if a contaminated package ever does make it into a running service. SBOM-level enforcement ties the whole thing together so any install that does not match your published SBOM fails the admission check.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.