Why Dependency Names Are a Security Boundary
Supply chain attacks are a persistent threat in modern JavaScript development, and npm is a common target. The good news is that a handful of configuration habits can meaningfully reduce your exposure when using npm. The core principle: never let a dependency name silently resolve to code you didn't intend to fetch.
Use Scopes for Internal Packages
In npm, a scope is an @-prefixed qualifier at the start of a package name, such as @my-company/foo. Scoped packages behave exactly like any other module name in package.json and in your JavaScript source. They were introduced in 2014, and every popular npm client supports them today.
{
"name": "@mycompany/foo",
"version": "1.2.3",
"description": "just a scoped package name example",
"dependencies": {
"@mycompany/bar": "2.x"
}
}
// es modules style
import foo from '@mycompany/foo'
// commonjs style
const foo = require('@mycompany/foo')
Scoped packages on the public npm registry can only be published by the user or organization that owns the scope, and you can make those packages private. Crucially, you can tie a scope to a specific registry. For example, this login command ensures that all requests for packages under @mycompany go to your internal registry, while everything else falls through to the default registry:
$ npm login --scope=mycompany --registry=https://registry.mycompany.local
That command writes the mapping to your ~/.npmrc:
@mycompany:registry = https://registry.mycompany.local/
//registry.mycompany.local:_authToken = xyzabc123-arbitrary-token-value
Next, create a free organization with the name mycompany on the public npm registry. Once that exists, no one else can publish anything under that scope publicly. Any misconfiguration in your internal projects will now fail with a 404 error instead of silently pulling untrusted code. This closes the door on an attacker squatting on your organization's name, which would otherwise expose you to the same attack.
As a final safeguard, place a .npmrc file at the root of each project containing:
@mycompany:registry = https://registry.mycompany.local/
This binds the scope to your internal registry for everyone who works in that repository.
Scope Avoids Collisions Too
Beyond security, scopes eliminate a class of non-malicious but painful resolution failures. Suppose you have an internal package called foo. Later, someone publishes a package named foo to the public registry, and a third party publishes bar, which depends on that public foo. If your tooling only points at the internal registry, installing bar will resolve bar's dependency to your internal foo—completely different code than what bar expects—and your build breaks unexpectedly.
Scopes also prevent accidental public exposure. If a developer mistakenly tries to publish a project that holds a scope you don't own publicly, the publish fails on permissions. These failures are frustrating rather than malicious, but they are avoidable with scopes.
Scope Closes One Vector, Not All
Scopes mitigate the specific attack where a public name shadows or is shadowed by your internal package. If your private registry product doesn't support scopes, push your vendor to add it—this is a supply chain security feature, not just a naming convenience.
If renaming all of your internal packages to a scope is a large migration, there are interim measures you should take while you plan that work.
Riskier Setup: Proxying Without Scopes
A common architecture is an internal registry that proxies packages from the public registry whenever it doesn't have a requested name locally. You publish private packages there, and the proxy "shadows" the public registry. With scopes in place, this is generally safe. Without them, you need to be more careful.
Never Proxy Names You Already Publish
First, verify that your internal registry does not proxy any package name that has already been published internally. If an attacker publishes a same-named package to the public registry and your internal registry would fetch it on request, you're exposed.
Also avoid registries that "merge" manifests from upstream for same-named packages. That feature is sometimes used to resolve collisions, but it is precisely how name hijacking works, so treat it as a deal-breaker. An implementation without this feature is preferable. Even without malicious actors, this defense doesn't help against the non-malicious resolution collisions described above—those can still be costly.
Project-Level Registry Config
Second, pin every internal project's registry with a .npmrc in the repository root:
registry = https://registry.my-company.local/
Without this, a fresh checkout in a developer environment or a CI runner might execute npm install and pull unexpected content from the public registry. Likewise, it prevents accidental publishes to the public registry. You can inspect the current setting with:
$ npm config get registry
Keep Internal Packages Immutable
Once a package has been published to your proxy registry, don't let it vanish silently or fall back to the public source. If you delete an internal package and the proxy then serves that name from the public registry, an attacker can claim the name and reach any leftover systems. This mirrors the public npm registry's strict removal rules: a removed version can never be reused or republished.
Trust Your Proxy Only as Much as Its Upstream
npm's CLI gives no special trust to the public registry beyond it being the default. If you override the registry config, npm uses your value. Your internal proxy is only as trustworthy as the data it proxies; if you permit upstream manifest merging or proxying of internal names, you inherit the public registry's weakest link.
Build Failures Are the Canary
With this configuration, a misrouted package resolves to a 404 error rather than untrusted content. Do not dismiss these failures. Build and deployment systems should fail loudly when a fetch goes wrong. Fix them immediately when they surface—supply chain attacks depend on errors being ignored. Failure alarms are an active security control; ignoring them discards one of your best defensive signals.
Adopt the Defaults That Fit
Consistent use of scopes, pinned registry configuration, and treating proxied content as untrusted goes a long way toward a more secure supply chain. Tooling like GitHub Packages can enforce several of these best practices as defaults, and the npm command line interface's roadmap is building more safeguards to make secure builds the easy path.



