Skip to content

[BUG] Registry-tarball allow-remote exemption fails when the configured registry origin differs from the lockfile resolved origin (proxy/mirror) #9548

Description

@manzoorwanijk

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

Current Behavior

With allow-remote set to none or root (the none default on npm 12), a npm install fails with EALLOWREMOTE on ordinary registry dependencies whenever the configured registry origin differs from the resolved URL origin recorded in the lockfile:

npm error code EALLOWREMOTE
npm error Fetching packages of type "remote" have been disabled

This is the common proxy/mirror case: a committed package-lock.json whose resolved URLs point to https://registry.npmjs.org/..., while the machine (or CI) is configured to use a private registry proxy/mirror with a different origin. Every registry tarball is then misclassified as a genuine remote tarball and blocked.

It affects both the hoisted and the linked install strategy — it is not strategy specific. The same install succeeds when allow-remote=all, or when the configured registry origin matches the resolved origin.

Expected Behavior

A tarball whose resolved URL is a registry-mediated URL (e.g. https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz) should be exempt from the allow-remote gate, identically to when the configured registry happens to share that origin. Configuring a private proxy/mirror should not turn every committed-lockfile registry dependency into a blocked remote tarball.

Steps To Reproduce

REPRO=/tmp/allowremote-proxy-repro
rm -rf "$REPRO" && mkdir -p "$REPRO" && cd "$REPRO"

cat > package.json << 'EOF'
{ "name": "x", "version": "1.0.0", "dependencies": { "minimatch": "3.1.5" } }
EOF

# 1) Generate the lockfile against the public registry, so resolved -> registry.npmjs.org
npm install --registry=https://registry.npmjs.org/ --allow-remote=none --no-audit --no-fund
# -> added 4 packages (configured origin == resolved origin, so the exemption fires)

# 2) Keep the lockfile, drop only node_modules, reinstall against a different registry origin.
#    The committed resolved URLs still point at registry.npmjs.org while the configured
#    registry origin is the mirror -> EALLOWREMOTE.
rm -rf node_modules
npm install --registry=https://registry.npmmirror.com/ --allow-remote=none --no-audit --no-fund
# -> npm error code EALLOWREMOTE

# 3) allow-remote=all bypasses it (confirms the cause):
rm -rf node_modules
npm install --registry=https://registry.npmmirror.com/ --allow-remote=all --no-audit --no-fund
# -> added 4 packages

Important: do not delete package-lock.json before step 2 — regenerating it against the mirror rewrites resolved to the mirror origin, which removes the mismatch and hides the bug. The mismatch requires a lockfile whose resolved origin differs from the configured registry origin (the normal committed-lockfile + proxy/mirror case). The mirror does not need to actually serve the tarball — the gate fires at extract time before/at fetch, purely from the origin comparison.

Root Cause

The registry-tarball exemption at reify.js:716 only overrides allowRemote: 'all' when #isRegistryResolvedTarball(node) returns true. That check at reify.js:849 compares the resolved URL origin against the configured registry origin:

#isRegistryResolvedTarball (node) {
  if (!node.resolved || !node.isRegistryDependency) {
    return false
  }
  try {
    const resolved = new URL(node.resolved)
    const registry = new URL(pickRegistry(npa(node.name), this.options))
    const registryPath = registry.pathname.replace(/\/?$/, '/')
    return resolved.origin === registry.origin &&
      (registryPath === '/' || resolved.pathname.startsWith(registryPath))
  } catch {
    return false
  }
}

When a proxy/mirror is configured, pickRegistry() returns the proxy origin while node.resolved is the canonical registry.npmjs.org URL from the lockfile, so resolved.origin === registry.origin is false. The override is skipped, pacote re-parses the name@URL spec as type=remote, and canUse() (pacote/lib/fetcher.js) throws EALLOWREMOTE.

Verified with a temporary probe in #isRegistryResolvedTarball (since reverted): for a hoisted minimatch install with a proxy registry configured, isRegistryDependency=true, resolvedOrigin=https://registry.npmjs.org, registryOrigin=<proxy origin> -> returns false. Switching the configured registry to https://registry.npmjs.org/ flips it to true and the install succeeds.

This is distinct from #9494/#9495 (store nodes lacking isRegistryDependency) and #9509 (genuine remote root tarballs). Here isRegistryDependency is correctly true; the failure is purely the configured-vs-resolved origin comparison.

Environment

  • npm: 12.0.0-pre.0 (latest); also present on 11.x when allow-remote is set to none/root (default all masks it)
  • Node.js: v24.15.0
  • OS Name: Darwin 25.5.0 (macOS, arm64)
  • System Model Name: Mac17,6
  • Relevant config:
registry = "https://<private-proxy-host>/<path>/"   ; origin differs from lockfile resolved URLs
allow-remote = none                                 ; default on npm 12

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions