Preventing dependency confusion with scoped registries and lockfile security
Learn how to prevent dependency confusion by enforcing scoped registries and verifying lockfile integrity in Node.js and PHP. Secure your supply chain today.
We spent about two days last month debugging a CI/CD pipeline that was pulling a malicious package version from a public registry instead of our private internal one. It’s a classic case of dependency confusion, and honestly, it’s one of the easiest ways for a project to get compromised if you aren't paying attention to your package manager's resolution logic.
If you don't explicitly tell your package manager where to look for specific packages, it will happily pull from the source that reports the highest version number. That’s a recipe for disaster.
Why Dependency Confusion Happens
The core of the problem is that package managers like npm or Composer are designed for convenience, not necessarily for strict security by default. When you request a package, the client often queries multiple registries—internal and public—simultaneously. If an attacker publishes a package with the same name as your internal one but with a higher version number, your build system might prioritize the public one.
We first tried solving this by simply adding our private registry to the top of our configuration files. It broke almost immediately because of transitive dependencies that still relied on the public registry. We had to rethink our approach to dependency confusion and move toward rigid, scoped configurations.
Hardening Your Scoped Registries
The most effective way to prevent this is to enforce scope-based mapping. By using namespaces (scopes), you tell your package manager that any package starting with @mycompany/ must come from your internal registry, and nothing else.
For npm (v7+), you should use an .npmrc file in your project root:
TEXT@mycompany:registry=https://registry.internal.example.com/ always-auth=true
In Composer for PHP, you define this in composer.json under the repositories key. Don't just list your internal registry; explicitly exclude the public packagist for those specific namespaces if you're using a proxy:
JSON{ "repositories": [ { "type": "composer", "url": "https://satis.internal.example.com" }, { "packagist.org": false } ] }
This forces the client to ignore the public repo for your private packages, which is a massive win for supply chain security.
The Role of Lockfile Integrity
Even with scoped registries, you aren't fully safe if your lockfiles aren't being verified. A lockfile (package-lock.json or composer.lock) ensures that every developer and CI runner uses the exact same version and hash of a dependency.
If an attacker manages to swap a package on a registry, the hash in your lockfile won't match the new package, and the install will fail. This is your last line of defense in package management.
| Tool | Lockfile Name | Integrity Feature |
|---|---|---|
| npm | package-lock.json | integrity (SHA-512) |
| Yarn | yarn.lock | resolved + integrity |
| Composer | composer.lock | content-hash |
Always commit your lockfiles to version control. If you're using CI, run npm ci or composer install --no-interaction --frozen-lockfile instead of the standard install commands. These commands fail if the lockfile is out of sync or if the hashes don't match, preventing silent dependency hijacking.
Putting It Into Practice
When we audited our own systems, we realized that while we had scoped registries, our developers were still using local .npmrc files that weren't checked into the repo. This meant our CI environment was using different resolution rules than our local machines.
To fix this, we standardized our environment configuration. We also implemented automated auditing, which you can read more about in Dependency Confusion Attacks: Securing Your Node.js and PHP Supply Chains.
Remember that lockfile integrity is only useful if you actually check the files. If you find yourself ignoring lockfile diffs in pull requests, you’re missing a critical signal that a dependency might have been tampered with or updated unexpectedly.
FAQ
Q: Should I use a private proxy for all dependencies? A: Yes. Using a tool like Artifactory or Verdaccio to proxy all traffic allows you to cache public packages and block malicious ones at the gateway.
Q: Does npm audit catch dependency confusion?
A: Not always. npm audit looks for known vulnerabilities in the version tree, but it doesn't necessarily know that a package was "confused" with a public malicious one.
Q: Is it safe to use npm install in production?
A: Never. Always use npm ci to ensure strict adherence to your lockfile.
We’re still debating whether to move to a fully locked-down, air-gapped registry approach for our most critical microservices. It adds overhead, but for high-security environments, it might be the only way to sleep soundly. For now, strict scoping and locked-down CI pipelines have cut our supply chain risks significantly. Just don't forget to keep your dependency audits consistent with the guidance in Supply Chain Security: Hardening npm postinstall and Composer Scripts.