Skip to content

install: honor .npmrc //host/:_authToken= for tarballs on a different host than the registry - #34329

Closed
robobun wants to merge 5 commits into
mainfrom
claude/farm/6d12cd40/install-tarball-auth-host
Closed

install: honor .npmrc //host/:_authToken= for tarballs on a different host than the registry#34329
robobun wants to merge 5 commits into
mainfrom
claude/farm/6d12cd40/install-tarball-auth-host

Conversation

@robobun

@robobun robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Problem

1.4 added a same-origin guard in NetworkTask::for_tarball: the registry Authorization header is only attached when the tarball URL origin matches the configured registry origin. That guard is correct (a malicious registry could otherwise point dist.tarball at an attacker host and harvest the scope token), but it breaks legitimate setups where the registry serves manifests from one host and tarballs from a separate authenticated host (Artifactory / Nexus / self-hosted with a CDN). There was no config escape hatch: a .npmrc //cdn-host/:_authToken= line is parsed but then dropped in the .npmrc loader because it matches neither the default registry nor any scoped registry.

1.3.14: CDN /probe-pkg-1.0.0.tgz auth=Bearer sekret  ->  1 package installed, EXIT=0
1.4:    CDN /probe-pkg-1.0.0.tgz auth=-              ->  error: GET .../probe-pkg-1.0.0.tgz - 401, EXIT=1

What npm does

npm-registry-fetch resolves auth by the URL being fetched, not by the package scope. getAuth(uri) nerf-darts the fetch URL (//host/path, walking path segments up to //host/) and uses whatever _authToken / _auth / username:_password is configured for that key. If nothing is configured for the tarball host, and sameHost(tarball, registry) is false, npm sends no Authorization at all (and on a 4xx warns the user to add a //<tarball-host>/:_authToken entry). So npm already has the same cross-origin protection, but honors an explicit host-keyed .npmrc entry.

Fix

  • .npmrc //host/path/:*= entries that match neither the default nor any scoped registry are kept in BunInstall::tarball_url_auth (and resolved into Options::tarball_url_auth via Scope::from_api, which handles $ENV_VAR expansion and username/password -> Basic).
  • NetworkTask::for_tarball keeps the existing same-origin gate; on a miss it looks up the tarball URL against those entries by host (including port) and path prefix, sending that entry's credential instead.
  • The same-origin guard is unchanged: a registry still cannot forward the scope credential to any host the user has not explicitly configured.

Tests

  • should send .npmrc //host/:_authToken= to a tarball host that is not the registry: registry on port A, authenticated CDN on port B, an unconfigured third host on port C. CDN receives Bearer cdn-token; the third host receives no Authorization.
  • should send .npmrc //host/:_auth= Basic auth to a tarball host that is not the registry: username + _password entries for the CDN host produce a Basic header.
  • The existing should send .npmrc _authToken on same-origin tarball download and withhold it cross-origin test still passes (cross-origin host with no .npmrc entry receives no auth).

Both new tests fail with USE_SYSTEM_BUN=1 (cdnAuth: [null], 401) and pass with bun bd test.

Docs: added a short section to docs/pm/npmrc.mdx explaining the tarball-host escape hatch.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install.test.ts

Fixes #30513

… host than the registry

1.4 added a same-origin guard on the registry Authorization header for
tarball downloads: a manifest's dist.tarball can point anywhere, so a
malicious registry could otherwise exfiltrate the scope token to an
attacker-controlled host. That guard also breaks legitimate setups
(Artifactory/Nexus with a separate tarball/CDN host) with no config
escape hatch, because .npmrc //cdn-host/:_authToken= lines that do not
match a configured registry were parsed and then dropped.

npm's own model (npm-registry-fetch getAuth) is: look up auth by the URL
being fetched. If the tarball host differs from the registry host, npm
sends no Authorization unless the user has an explicit //tarball-host/
nerf-dart entry, in which case it sends that entry's credential.

Keep .npmrc //host/:*= entries that match no registry and consult them
in for_tarball when the tarball origin differs from the scope registry
origin. The same-origin guard is unchanged: a registry still cannot
forward scope credentials to a host the user has not configured.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3868b76a-e165-43d9-a5a1-33b28b41cbd5

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbe075 and d45c404.

📒 Files selected for processing (6)
  • docs/pm/npmrc.mdx
  • src/ini/lib.rs
  • src/install/NetworkTask.rs
  • src/install/PackageManager/PackageManagerOptions.rs
  • src/options_types/schema.rs
  • test/cli/install/bun-install.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:47 AM PT - Jul 16th, 2026

@robobun, your commit d45c404 has 2 failures in Build #73846 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34329

That installs a local version of the PR into your bun-34329 executable, so you can run:

bun-34329 --bun

@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Reproduced with a registry on one port serving manifests and an authenticated CDN on another port serving tarballs; with a .npmrc //cdn-host/:_authToken= line present, 1.4 sends no Authorization to the CDN and exits 1 with error: GET .../probe-pkg-1.0.0.tgz - 401.

All three new tests fail with USE_SYSTEM_BUN=1 bun test test/cli/install/bun-install.test.ts -t 'tarball host that is not the registry|direct URL tarball dependency' and pass with bun bd test. The existing same-origin/cross-origin test is unaffected.

CI: the diff's tests pass on every lane that ran. Build #73846 is red on test-net-connect-memleak.js (pre-existing on main, alpine x64) and an Azure agent-creation failure for the windows-2019 lane; neither touches the install/.npmrc paths this PR changes. Ready for review.

@github-actions

Copy link
Copy Markdown
Contributor

Found 4 issues this PR may fix:

  1. auth tokens not sent to scoped-registry tarball URLs when token path diverges from registry URL path (regression from 1.3.10) #30513 - Auth tokens not sent to tarball URLs when the token path diverges from the registry URL path; this PR's tarball_url_auth longest-prefix matching directly addresses this
  2. Authorization headers not sent for authenticated registry tarball(.tgz) installs #26462 - Authorization headers not sent for authenticated registry tarball installs (Nexus); the new cross-origin tarball auth fallback would help when configured via .npmrc
  3. _authToken with with scopes stopped working in bun v1.3.11 works in 1.3.10 #28233 - _authToken with scopes stopped working in v1.3.11 due to over-strict path matching; the tarball_url_auth fallback restores auth for divergent paths
  4. Private GitLab install errors #26241 - Private GitLab install errors with multiple scoped registries where token paths don't match registry paths

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #30513
Fixes #26462
Fixes #28233
Fixes #26241

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. install: resolve .npmrc credentials by path-segment ancestor #33869 - Also implements npm's nerf-darting credential resolution so that .npmrc //host/:_authToken= entries are applied to tarball (and manifest) URLs that don't exactly match a configured registry, solving the same 401 problem with a more comprehensive ancestor-walk rewrite of the credential resolution system.

🤖 Generated with Claude Code

@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Re the duplicate/related-issue bot comments:

#33869 is complementary, not a duplicate. It rewrites .npmrc credential resolution so //host/:_authToken= applies to a registry at https://host/some/path/ via npm's path-ancestor walk (manifest and same-host tarball auth). It does not touch NetworkTask::for_tarball, so the same-origin gate there would still strip auth for a tarball on a different host than the registry, which is the case this PR addresses. The two will conflict in src/ini/lib.rs; whichever lands second needs a small rebase.

Related issues: of the four flagged, only #30513 is directly fixed here (it is specifically about tarball URLs whose path diverges from the scoped registry path; the tarball_url_auth longest-prefix lookup covers it). #28233 and #26241 fail at the manifest fetch, which #33869 addresses. #26462 uses a bunfig.toml scope for a direct URL-tarball dependency and would need an explicit .npmrc //host/ entry to benefit from this PR.

Comment thread src/install/NetworkTask.rs
Comment thread test/cli/install/bun-install.test.ts Outdated
Comment thread src/install/NetworkTask.rs
…arball test

- tarball_url_auth_for: match a portless .npmrc key against a tarball URL
  whose port is the scheme default (npm builds the key from WHATWG
  URL.host, which strips default ports)
- rename the Basic-auth test to reflect what it exercises
  (username+_password, not _auth)
- add a test for direct-URL tarball dependencies (NoAuthorization path)
  with a matching .npmrc //host/ entry
- doc comment on Authorization::NoAuthorization clarifying it gates only
  the registry-scope credential

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline nit: the three fixes in e89bba7 look right — default-port normalization now mirrors the same-origin check, the Basic-auth test is renamed, and the direct-URL tarball path is covered with a test + doc comment on NoAuthorization. Also confirmed the security model is unchanged: the registry-scope credential still only crosses the same-origin gate, and tarball_url_auth_for only ever returns a credential the user explicitly keyed to that host in .npmrc. Leaving final sign-off to a human given this is credential-routing code and overlaps with #33869.

Extended reasoning...

The author addressed all three of my prior inline nits in e89bba7; I verified each fix in the current diff. This run surfaced one additional narrow nit (stale tarball_url_auth entry across multi-file .npmrc loads), which is posted inline. I checked that the new tarball_url_auth_for path cannot leak a credential to an unconfigured host — every returned scope comes from an entry the user wrote in .npmrc for that exact host (with default-port normalization), and the existing same-origin guard on the registry-scope token is untouched. Not approving because this is auth/credential routing in the package manager, and there is a design-level overlap with #33869 that a maintainer should reconcile.

Comment thread src/ini/lib.rs
The configs vec accumulates across .npmrc files and is re-iterated in
full each call; clearing tarball_url_auth before the loop ensures an
entry that was unmatched in the global file is not left stale when the
project file introduces a scoped registry at the same host.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four nits from earlier review rounds are addressed (default-port normalization, test rename, direct-URL tarball test + NoAuthorization doc comment, and the tarball_url_auth.clear() before the configs re-iteration). No further issues found. Deferring to a human because this changes credential-attachment logic in the package manager — worth a second pair of eyes on the host/path-prefix matching and the NoAuthorization semantics change, and on how this should land relative to #33869 (which will conflict in src/ini/lib.rs).

What was reviewed:

  • tarball_url_auth_for host+port match and path-segment-boundary prefix check — no substring/partial-segment hazard; longest-prefix wins.
  • Same-origin guard is unchanged; the unrelatedAuth: [null] assertion covers the no-leak-to-unconfigured-host case.
  • apply_config_opt extraction is a byte-identical dedup of the two existing match blocks.
  • .clear() before the configs loop keeps multi-.npmrc ordering consistent with the scoped-registry re-iteration.
Extended reasoning...

Overview

The PR adds an escape hatch to the 1.4 same-origin guard for tarball downloads: .npmrc //host/path/:*= entries that don't match any configured registry are now retained in a new tarball_url_auth list, and NetworkTask::for_tarball consults it when the tarball origin differs from the registry origin. Touches src/ini/lib.rs (loader + new apply_config_opt helper deduped from two identical match blocks), src/install/NetworkTask.rs (new tarball_url_auth_for lookup + rework of the send_auth gate into an auth_scope: Option<&Scope>), PackageManagerOptions.rs / schema.rs (plumbing the new field), plus three new tests and a docs section.

Security risks

This is credential-handling code. The core question is whether a credential can be sent to a host the user did not explicitly name. I checked:

  • The same-origin guard on the registry-scope credential is unchanged — a registry still cannot exfiltrate the scope token to an arbitrary dist.tarball host.
  • The fallback only sends a credential to a host that appears verbatim as a .npmrc //host/ key the user wrote. Host comparison is exact (including port, with one-sided default-port normalization on the tarball side); path is a segment-boundary prefix (guarded by the == b'/' check), so //host/foo/ does not match /foobar/....
  • The first new test asserts unrelatedAuth: [null] for a third host with no .npmrc entry, which is the negative contract.
  • Authorization::NoAuthorization now consults tarball_url_auth too — a semantics change, but again only for hosts the user explicitly configured; matches npm-registry-fetch behavior for direct-URL tarballs and now has both a doc comment and a dedicated test.

I did not find a leak vector, but this is exactly the surface where a subtle matching bug becomes a credential disclosure, so human review is appropriate.

Level of scrutiny

High. Package-manager auth-header attachment is production-critical and security-sensitive. The implementation is well-scoped, thoroughly commented, and mirrors npm's documented behavior, but the guidelines say not to approve auth/credential changes without human review.

Other factors

  • Four prior nits from earlier review rounds were all addressed in e89bba7 and 5935d02; all threads are resolved.
  • There is an overlapping open PR (#33869) that rewrites .npmrc credential resolution more broadly and will conflict in src/ini/lib.rs; a maintainer should decide sequencing.
  • Test coverage is good: cross-origin CDN with Bearer, username+_password → Basic, direct-URL tarball, and the existing same-origin/cross-origin negative test still applies. Tests use in-process servers with port: 0, drain pipes concurrently, and assert exact header values.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four earlier nits are addressed (default-port normalization, test rename, direct-URL tarball test + NoAuthorization doc comment, and the .clear() for multi-file .npmrc staleness) and I found nothing new on this pass. Deferring to a human because this is credential-forwarding logic on the install hot path, and the overlap with #33869 (which rewrites the same .npmrc credential resolution in src/ini/lib.rs) is a landing-order call a maintainer should make.

What was reviewed:

  • for_tarball auth-scope selection — same-origin gate is unchanged; cross-origin fallback only consults user-written //host/:*= entries, and the negative test asserts an unconfigured third host receives no Authorization.
  • tarball_url_auth_for host/port match and path-prefix logic, including the segment-boundary check (tarball_path[entry_path.len()] == b'/') so /foo does not match /foobar.
  • .npmrc loader: the apply_config_opt extraction is byte-identical to the two removed match blocks; tarball_url_auth is now cleared and rebuilt each pass so project-over-global precedence holds.
Extended reasoning...

Overview

Adds an escape hatch for the 1.4 same-origin tarball auth guard: .npmrc //host/path/:*= entries that match neither the default nor any scoped registry are retained in a new BunInstall::tarball_url_auth list, resolved into Options::tarball_url_auth via Scope::from_api, and consulted by NetworkTask::for_tarball when the tarball URL is cross-origin from the registry (or when Authorization::NoAuthorization is passed for direct-URL tarball deps). Touches src/ini/lib.rs (loader + apply_config_opt dedup), src/install/NetworkTask.rs (tarball_url_auth_for + for_tarball), PackageManagerOptions.rs / schema.rs (new field), three tests in bun-install.test.ts, and a docs section.

Security risks

The relevant risk is credential leakage to a registry-controlled host. The design is sound: the same-origin guard on the registry-scope credential is unchanged, and the fallback only ever returns a Scope the user explicitly keyed by host in .npmrc. A malicious registry pointing dist.tarball at an attacker host still receives nothing unless the user themselves wrote a //attacker/:_authToken= line. The first new test asserts exactly this (unrelatedAuth: [null]). The NoAuthorization branch now sending a matching //host/ credential to a direct-URL tarball is a strict improvement (matches npm) and is covered by the third test. I did not find a way to route a credential to a host the user did not name.

Level of scrutiny

High — this is Authorization-header attachment in bun install's network path, squarely in the "security-sensitive code (auth)" bucket where the guidelines say not to auto-approve. It also intersects #33869, which rewrites credential resolution more broadly; whichever lands second needs a rebase in src/ini/lib.rs, and a maintainer should decide sequencing.

Other factors

I reviewed this PR twice previously and left four nits, all now addressed and marked resolved. The bug-hunting system found nothing on this run. The apply_config_opt refactor is a pure extraction of two identical match blocks. Tests are hermetic (local Bun.serve on port: 0), assert exact header values, include the negative case, and cover both AllowAuthorization and NoAuthorization paths plus Bearer and Basic. Net: I think this is correct, but auth-header changes plus the #33869 coordination warrant a human sign-off.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #38800, which resolves .npmrc //host/path/ credential lines by request URL in general and includes the case this PR handled: a registry whose dist.tarball URLs point at another host that has its own //host/ line gets that line's credentials (bearer token or username/_password), and the registry's own credentials are still never sent to another host.

Checked against a build of #38800's branch: the two cross-host tarball tests from this PR pass on it unmodified (both still fail on current main, which has neither change), and a basic-auth variant of the cross-host case has been added to #38800's test block. The one thing here that #38800 does not carry forward is sending //host/ lines to dependencies written as a tarball URL in package.json; #38800 deliberately leaves those credential-free, as they are today, so that would be a separate change if wanted. #30513 stays tracked by #38800.

@robobun robobun closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auth tokens not sent to scoped-registry tarball URLs when token path diverges from registry URL path (regression from 1.3.10)

1 participant