Skip to content

[BUG] npm install fails with EALLOWREMOTE on certain private registry configurations #9796

Open

Description

@baconwaifu

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

This is not just a request to bump a dependency for a CVE

  • This is not solely a request to bump a dependency for a CVE

Current Behavior

If a registry is serving tarballs from a different path than the actual registry metadata (such as https://registry.example.com/blob/package.tgz, while the registry is https://registry.example.com/npm) this results in EALLOWREMOTE attempting to install anything from it.

Despite these ostensibly being the same origin to most browsers, as both the hostname and protocol match, npm apparently wants registry= to be an exact prefix for the purposes of allow-remote=none, yet I can't find anywhere that expectation is documented.

Expected Behavior

One of the following:

NPM follows browser rules, and understands that if the hostname and protocol match, the URL is indeed from the configured registry.

The expectation that tarballs are hosted/proxied from a path under the root of the registry, if the registry is not already the root of the site, is documented somewhere easily accessible.

Steps To Reproduce

  1. Set up a registry that serves tarballs from a different directory on the same hostname (such as aliasing two different paths to the same registry instance with a reverse proxy? the actual registry in question has something similar, though I'm not in a position to know the exact config behind it)
  2. Point NPM at the "non-canonical" registry path (so that the returned tarball path isn't underneath it)
  3. Run npm install, get EALLOWREMOTE.

Environment

  • npm: 12.0.1
  • Node.js: 24.18.0
  • OS Name: MacOS
  • System Model Name: Macbook Pro
  • npm config: (Actual registry URL redacted for privacy, but the paths are accurate)
; "user" config from /Users/<redacted>/.npmrc
<redacted auth tokens>

; registry = "https://registry.example.com/api/npm/npm" ; overridden by project

; "project" config from /Users/<redacted>/<redacted>/.npmrc

registry = "https://registry.example.com/api/npm/npm" ; The same one as above

The registry itself is a JFrog Artifactory instance I'm not in a position to know any precise details about, with the actual paths involved being /api/npm/npm (The one in all our documentation and configs) and /artifactory/api/npm/npm (the one that the tarballs resolve under). Changing our config to use the more cumbersome path 'fixes' the issue, but is somewhat annoying if we have to rewrite everything just because someone decided that folders should be trust boundaries now.

Activity

  1. simonmysun commented on Aug 12, 2026

    @simonmysun

    We hit EALLOWREMOTE on npm 12.0.2 as well, and while digging into it I found what looks like a second, related trigger for the same underlying bug. Sharing the analysis in case it helps triage.

    TL;DR

    npm 12 has a compensating patch in Arborist (#isRegistryResolvedTarball) that is supposed to prevent allow-remote from mis-firing on registry-hosted tarballs. That patch has (at least) two blind spots:

    1. The one this issue reports: the tarball path isn't a prefix of the configured registry= URL (reverse-proxied / multi-path registries).
    2. A second one: npm: aliases pointing at a scoped package on a custom registry, where the alias name loses the scope and the registry lookup falls back to the default registry.

    Both end up wrongly classified as type: 'remote' even though the tarball is served by a configured registry.

    The mechanism (npm 12.0.2, @npmcli/arborist)

    During reify, the node is turned into a spec of the form `${node.name}@${resolvedURL}`:

    // @npmcli/arborist/lib/arborist/reify.js  (~line 711)
    res = `${node.name}@${registryResolved}`

    pacote's npa then re-parses name@URL as type: 'remote' (there's even a comment acknowledging this):

    // reify.js ~line 750
    // pacote's npa re-parses our `name@URL` spec as type=remote, so allowRemote would mis-fire on registry tarballs.
    ...(this.#isRegistryResolvedTarball(node) ? { allowRemote: 'all' } : {}),

    That routes it to the remote branch instead of the alias/version → allowRegistry branch:

    // pacote/lib/fetcher.js ~line 501
    case 'remote':
      canUse({ allow: opts.allowRemote, ... })   // <-- allow-remote=none throws EALLOWREMOTE
    ...
    case 'version':
    case 'range':
    case 'tag':
    case 'alias':
      canUse({ allow: opts.allowRegistry, ... }) // <-- where it *should* land

    So the allow-remote gate only stays correct if #isRegistryResolvedTarball(node) returns true. Here's that function:

    // reify.js ~line 988
    #isRegistryResolvedTarball (node) {
      if (!node.resolved || !node.isRegistryDependency) return false
      try {
        const resolvedURL = new URL(this.#registryResolved(node.resolved))
        // pickRegistry only consults spec.scope, so a bare-name (tag) parse is sufficient...
        const registry = new URL(pickRegistry(npa(node.name), this.options))
        const registryPath = registry.pathname.replace(/\/?$/, '/')
        return resolvedURL.origin === registry.origin &&
          (registryPath === '/' || resolvedURL.pathname.startsWith(registryPath))
      } catch {
        return false
      }
    }

    The reporter's case fails on the pathname.startsWith(registryPath) check (tarball path isn't under the registry= path).

    The alias variant

    pickRegistry(npa(node.name), ...) only consults spec.scope. For a npm: alias, node.name is the alias name, not the target package name — so the scope is lost.

    Concrete repro. Two dependencies on the same private registry (a GitLab package registry):

    // package.json
    "@<some-org>/<some-package>": "<some-version>",                              // normal scoped dep -> works
    "<some-alias-package>": "npm:@<some-org>/<some-alias-package>@x.x.x-...",    // npm: alias        -> EALLOWREMOTE
    // .npmrc
    @<some-org>:registry=https://<host>/api/v4/projects/<xxx>/packages/npm/
    

    Both resolve to bare tarball URLs under the same registry origin+path. In clean-cache isolated installs, @<some-org>/<some-package> installs fine, while the alias throws:

    npm error code EALLOWREMOTE
    npm error Fetching packages of type "remote" have been disabled
    npm error Refusing to fetch "<some-alias-package>@https://<host>/api/v4/projects/<xxx>/packages/npm/@<some-org>/<some-alias-package>/-/@<some-org>/<some-alias-package>-x.x.x-...tgz"
    

    Simulating #isRegistryResolvedTarball with the real config shows exactly why:

    node.name npa(name).scope pickRegistry result isRegistryResolvedTarball
    @<some-org>/<some-package> @<some-org> @<some-org>:registry true → allowed
    <some-alias-package> undefined registry.npmjs.org (default) false → EALLOWREMOTE

    For the alias, npa("<some-alias-package>").scope === undefined, so pickRegistry falls back to the default registry, the origin no longer matches our own package registry, and the patch declines to grant the override.

    Suggested fix

    #isRegistryResolvedTarball should resolve the registry from the target package name, not the alias name. Using the real name makes the check pass:

    // instead of npa(node.name)
    const realName = node.package?.name || node.name
    const registry = new URL(pickRegistry(npa(realName), this.options))

    More broadly, the underlying issue for both cases is that allow-remote decisions are made after a registry-resolved dependency has been flattened into a name@tarballURL spec, so the "is this actually a configured registry?" reconstruction in #isRegistryResolvedTarball has to be watertight. It currently misses (1) tarball paths outside the registry= prefix and (2) aliases whose scope is dropped.

    Environment

    • npm: 12.0.2
    • Node.js: 26.7.0
    • Registry: JFrog/GitLab-style package registry serving tarballs at bare URLs under the registry path
  2. arseniyskvortsov-tempo commented on Aug 12, 2026

    @arseniyskvortsov-tempo

    The "alias" problem also happens with GitHub Packages (the registry is under /npm, the blobs are under /download).

  3. self-assigned this
    on Aug 27, 2026
  4. added
    cmd:installrelated to `npm install`
    ws:arboristRelated to the arborist workspace
    and removed
    Needs Triageneeds review for next steps
    on Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Bugthing that needs fixingPriority 1high priority issueallowRemotecmd:installrelated to `npm install`ws:arboristRelated to the arborist workspace

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions