npm scopes and private packages
If your organization uses internal npm packages, you almost certainly use npm scopes — names like @acme/auth-utils, @company/shared-components, or @myorg/data-client. Scopes are the primary mechanism for namespacing internal packages and, when configured correctly, are a strong defense against dependency confusion attacks.
The key words are "when configured correctly." Scoped packages that are not properly configured on the public npm registry remain vulnerable to dependency confusion.
How npm scopes work
An npm scope is a prefix that namespaces a group of packages. Scopes start with @ and are followed by a name:
{
"name": "@acme/auth-utils",
"version": "1.2.3"
}
When you install a scoped package, npm looks for it in the registry configured for that scope. The default registry is registry.npmjs.org. If your organization has a private registry for @acme, you configure it in .npmrc:
@acme:registry=https://your-private-registry.acme.com
With this configuration, npm install @acme/auth-utils goes to your private registry, not npm's public registry.
The misconfiguration that creates vulnerability
The vulnerability appears when one of two things is true:
The scope is not claimed on the public npm registry. If your company has never registered the @acme scope on registry.npmjs.org, anyone can create packages under @acme on the public registry. An attacker who publishes @acme/auth-utils@9.9.9 to the public registry can execute the dependency confusion attack.
The npm fallback behavior is not disabled. By default, if npm can't find a scoped package on the configured private registry, it falls back to the public registry. This fallback is the mechanism of dependency confusion.
How to fix both issues
Claim your scope on the public npm registry:
# Log in to npm
npm login
# Create an organization (this claims the scope)
npm org create acme
# Verify the scope is claimed
npm org ls acme
Once your organization owns the @acme scope on the public registry, no one else can publish packages under it. Dependency confusion against @acme packages becomes impossible for the scope-registration vector.
Disable the public registry fallback for your scope:
# .npmrc
@acme:registry=https://your-private-registry.acme.com
# This line prevents fallback to the public registry:
@acme:always-auth=true
Or use a more explicit configuration that errors if the private registry is unreachable, rather than falling back:
# For npm 9+
@acme:registry=https://your-private-registry.acme.com
# Set the scope's registry to only resolve from private
legacy-peer-deps=false
Alternatively, publish all internal packages to the public registry as private packages:
If your internal packages don't contain sensitive code, publishing them to the public npm registry as private packages (which only your organization's members can install) is the simplest solution — it claims the namespace and eliminates the private registry configuration complexity.
npm publish --access restricted # private npm package
What Veln adds
Even with correct scope configuration, dependency confusion attacks can still occur if:
- The scope was not claimed and an attacker registered it before you
- Your
.npmrcconfiguration is incorrect or inconsistently deployed
Veln's Tier 1 analysis compares the publisher identity of an installed package against the publisher identity of previously-seen versions of the same package. If @acme/auth-utils was previously installed from your private registry (published by your organization's account) and a new install shows it coming from a different account, Veln flags the publisher change as a high-risk signal.
Additionally, a package with the same name as an internal dependency but published by a brand-new npm account (the attacker's account) would score near zero on publisher account age, triggering a HOLD verdict regardless of the scope.
Claim your npm scope on the public registry. Then use Veln for the attacks scope registration can't prevent.