In 2021, a security researcher named Alex Birsan got code to execute inside the networks of Apple, Microsoft, Tesla, PayPal, Netflix, Uber, and dozens of other major companies — without exploiting a single traditional vulnerability. He didn't phish anyone. He didn't break any firewalls. He just uploaded packages to public registries with the same names as internal, private packages those companies used. Their build systems did the rest, automatically pulling his code instead of the trusted internal version.
That technique is called a dependency confusion attack, and three years later it's still catching agencies and dev teams off guard. If you manage build pipelines, npm/pip/composer dependencies, or CI/CD for client sites, this is one of the more quietly dangerous supply chain risks in modern web development.
What Is a Dependency Confusion Attack?
Most organizations use a mix of two types of packages:
- Public packages — pulled from registries like npm, PyPI, RubyGems, or Packagist
- Private/internal packages — custom code hosted on an internal registry, often with names like
@company/auth-utilsorinternal-logger
The attack works because most package managers, by default, don't strictly separate these two sources. When a build system is told to install internal-logger, it may check both the public registry and the private one — and depending on configuration, it can pick whichever version number is higher, regardless of source.
An attacker who discovers the name of your internal package (often leaked in a public GitHub repo, a job posting, a public error log, or a leaked package.json) simply publishes a package with the same name on the public registry with a higher version number. Many package managers will happily fetch the attacker's version instead of your internal one — and it runs with the same privileges as any other dependency, often during install via a postinstall script.
Why This Is So Effective
- It requires no access to your network, your VPN, or your credentials
- It exploits default, "correct" behavior of package managers — not a bug
- Internal package names leak constantly through open source repos, Slack integrations, job listings, and error messages
- The payload runs automatically during
npm installorpip install, often with CI/CD permissions attached
A Concrete Example
Say your agency builds internal tooling and names a private package acme-shared-components. It lives in a private npm scope for internal use. If this name ever appears in:
- A public GitHub repo's
package.jsonor lockfile - A public CI log (GitHub Actions, CircleCI, Jenkins output)
- A Stack Overflow question copy-pasted by a junior dev
- An old, forked, or archived repo nobody remembers exists
...an attacker can publish acme-shared-components version 99.0.0 to the public npm registry. If your build config doesn't explicitly lock the source, the next `npm install` on a laptop, staging server, or CI runner pulls the malicious package instead. From there, the attacker has a foothold — often enough to exfiltrate environment variables, tokens, and source code straight out of your CI pipeline.
How to Check If You're Exposed
1. Audit your package names for public availability
For every internal/private package your team maintains, check whether that exact name already exists on the public registry (npm, PyPI, RubyGems, Packagist, NuGet, Maven Central). If it does, and it's not yours — you're exposed right now.
2. Search your own repos and logs for leaked names
Grep your public repositories, forks, and CI logs for references to internal package names. Don't forget:
- Old forks or archived repos that predate your current security practices
- Public demo repos built from internal boilerplate
- Job postings or engineering blog posts that mention internal tool names
3. Review your package manager's resolution order
Check whether your .npmrc, pip.conf, or equivalent config explicitly scopes internal packages to your private registry only, rather than allowing fallback to the public one.
Fixing It: Practical Mitigations
Scope your private packages
For npm, use scoped packages (@yourcompany/package-name) and configure .npmrc to map that scope explicitly to your private registry:
@yourcompany:registry=https://your-private-registry.com/
This prevents the resolver from ever checking the public registry for anything under that scope.
Reserve your package names publicly
Even if you never intend to publish a package publicly, claim the name on the public registry as an empty placeholder. This blocks attackers from squatting on it. Many large companies now do this as standard practice.
Pin exact versions and use lockfiles properly
- Commit lockfiles (
package-lock.json,yarn.lock,poetry.lock) and enforce their use in CI (npm ciinstead ofnpm install) - Avoid wildcard or loose version ranges for internal dependencies
- Use checksums/integrity hashes where your package manager supports them
Restrict registry sources in CI/CD
Configure build agents to only pull from an approved, internal artifact proxy (e.g., a scoped Verdaccio, Artifactory, or GitHub Packages setup) rather than reaching out to public registries directly for anything sensitive.
Disable postinstall scripts where possible
A large share of dependency confusion payloads execute via postinstall hooks. Running installs with --ignore-scripts in CI (and only allowing scripts for explicitly vetted packages) removes a major execution path for attackers.
This Isn't Just an npm Problem
Dependency confusion affects any ecosystem with a public/private registry split:
- Python — PyPI, especially with internal packages referenced in
requirements.txt - Ruby — RubyGems, particularly for internal gems used in Rails monorepos
- PHP — Composer/Packagist, common in agency-built WordPress and Laravel projects
- .NET — NuGet, in enterprise environments with internal feeds
- Java — Maven Central, for internal artifacts in multi-module builds
If your agency works across multiple stacks for different clients, it's worth auditing each ecosystem separately — the mitigation steps look slightly different in each, but the underlying risk is identical.
Where This Fits Into Broader Site Security
Dependency confusion is a build-time and supply-chain risk, which means it often sits outside what a typical external security scan can see — but it's rarely the only issue on a site that has weak security hygiene overall. Sites with permissive CORS policies, missing CSP headers, or outdated SSL configs tend to be the same sites where dependency management is also an afterthought.
Running a free scan at WebSentry won't detect a dependency confusion vulnerability directly (that requires source-level auditing), but it will quickly surface the related weak spots — missing security headers, misconfigured CORS, weak CSP, exposed cookies — that tend to compound the damage if an attacker does get a foothold through a compromised package. Agencies managing multiple client sites often use WebSentry's A–F grading to prioritize which projects need a deeper supply-chain audit first: the sites already scoring poorly on headers and DNS configuration are usually the ones where nobody's checked package registry hygiene either.
If you haven't audited your internal package names against public registries this year, that's step one — before any build tool upgrade or dependency bump, check whether someone else already owns the name you think is private.
Check your own site
Run a free security scan and see if your site has the issues covered in this article. Results in under 30 seconds.
