Skip to content
← Blog

Technical explainer

npm scopes and private packages

2 min read

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 .npmrc configuration 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.