Skip to content

install: send credentials embedded in a tarball URL as Basic authorization - #39025

Closed
robobun wants to merge 1 commit into
mainfrom
farm/a6acfa69/tarball-url-credentials
Closed

install: send credentials embedded in a tarball URL as Basic authorization#39025
robobun wants to merge 1 commit into
mainfrom
farm/a6acfa69/tarball-url-credentials

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A dependency declared as a tarball URL with credentials, "no-deps": "http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz", is downloaded without an Authorization header. A server that needs the credentials answers 401 and the install fails with error: GET http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz - 401 (bun 1.4.0 and main, hoisted and isolated linker). npm 11 installs the same package.json and sends Authorization: Basic Y2Fyb2w6czNjcmV0 (base64("carol:s3cret")).
  • The username-only form (http://token@127.0.0.1:PORT/x.tgz) does not reach the server at all: the request goes to the hostname token@127.0.0.1.
  • Cause: NetworkTask::for_tarball (src/install/NetworkTask.rs) only ever attaches the registry scope's configured token or _auth, and only for npm packages whose tarball is on the registry's origin. Nothing reads the URL's userinfo, and the HTTP client does not either (it only turns the userinfo of a proxy URL into Proxy-Authorization). The misrouted username-only form comes from bun_url::URL::parse, which takes token@127.0.0.1 for the hostname when the userinfo has no : and a port follows (tracked separately; this change no longer depends on how that parser splits the authority).
  • Found while fixing the isolated store names of these URLs (install: leave URL credentials out of isolated store entry names #39014), not from a user report.

Fix

  • for_tarball splits the userinfo off the request URL before anything else looks at it (split_url_userinfo: the authority runs from :// to the first /, ? or #, the userinfo is everything in it up to the last @, so the @ of /@scope/pkg/-/pkg.tgz is not one) and sends it as Authorization: Basic base64(userinfo) (basic_authorization_from_userinfo, which appends : when the userinfo has no password). The request URL is the URL without the userinfo.
  • Precedence: when the registry scope's credentials apply to the tarball (npm package, tarball on the registry origin, scope has a token or _auth), they are still sent and the URL's are not. This is npm's order as well: npm-registry-fetch sets the header from the config, and node only derives Authorization from the URL's auth when the request has no such header.
  • Why Basic of the userinfo as written: it is what npm sends, checked against npm 11.16 for user:pass, user (sent as user:), :pass and a percent-encoded password (sent undecoded); transcript below. It is also how bun already treats credentials written into a registry URL (install: send credentials embedded in --registry and registry env var URLs #38796 stores the username and password bytes as given). pnpm was not available offline. Two spellings are deliberately not npm's: a second : inside the password is sent as written where npm percent-encodes it, and :token@ in a tarball URL is Basic like npm rather than the Bearer that bun's registry URLs make of it; both are called out in the test table and in the code comment.
  • Why the URL is requested without the userinfo: bun_url::URL::origin includes the userinfo and the HTTP client compares origins to decide whether Authorization follows a redirect, so with the userinfo left in, a redirect to the same host would drop the header (verified: with only the header added, the redirect test below fails with authorization: null on the second hop). It also fixes the username-only form without touching bun_url, and the GET <url> - 401 line now prints the URL without the credentials. The cross-host rule is unchanged: the HTTP client strips the header on a redirect to another origin, same as for the registry token (test included).
  • Behavior outside the request is unchanged: package.json, the lockfile, the task id and the cache key still use the URL as written. Documented in docs/pm/cli/add.mdx.
  • Verified with test/cli/install/bun-install.test.ts, describe("credentials embedded in a tarball URL"): 12 tests, 10 of which fail on the unfixed build (the two guards, scoped path and registry credentials taking precedence, pass on both). The rest of the file is unchanged: the remaining failures locally are the tests that need the public internet, identical with the unmodified binary.
  • cargo check -p bun_install, cargo clippy -p bun_install, rustfmt and prettier are clean.

Background

  • Tarball dependency: a package.json entry whose version is an http(s):// URL ending in .tgz/.tar.gz/.tar. Bun downloads it directly; no registry manifest is involved, so the registry's configured credentials never applied to it (Authorization::NoAuthorization at the call sites in PackageManagerEnqueue.rs). Registry packages reach the same for_tarball through their manifest's dist.tarball URL with AllowAuthorization, which is the case where the registry scope's credentials can apply.
  • Userinfo: the user:password@ part of a URL's authority (RFC 3986 section 3.2.1). HTTP never puts it on the wire; clients that honor it (npm through minipass-fetch, curl, browsers) convert it into Authorization: Basic base64(user:password).
  • NetworkTask::for_tarball builds one HTTP request per tarball download: url_buf (the request URL) and header_buf (the headers, built with HeaderBuilder in two passes, count then append, because the buffer is allocated exactly once in between). Retries reuse the same request, so the header is sent on every attempt.
  • bun_url::URL::parse is the allocation-free splitter the HTTP client and for_tarball use; it is not a WHATWG parser. Its origin is a prefix of the input string, which is why it still contains the userinfo.
npm 11.16 against a local server logging the Authorization header (same tarball, same package.json shape)
userinfo in the dependency URL   header npm sent
carol:s3cret@                    Basic Y2Fyb2w6czNjcmV0      = base64("carol:s3cret")
carol@                           Basic Y2Fyb2w6              = base64("carol:")
:s3cret@                         Basic OnMzY3JldA==          = base64(":s3cret")
carol:s3%40cret@                 Basic Y2Fyb2w6czMlNDBjcmV0  = base64("carol:s3%40cret"), not decoded
carol:s3:cret@                   Basic Y2Fyb2w6czMlM0FjcmV0  = base64("carol:s3%3Acret"), bun sends base64("carol:s3:cret")
carol:s3cret@ + 302 to same host Basic ... on both hops

bun 1.4.0-canary.1 (eabb96d) for the first row: the server logs auth=null and bun prints error: GET http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz - 401.

…ation

A dependency declared as http://user:pass@host/pkg.tgz (or a registry
dist.tarball URL of that shape) was requested without any Authorization
header. NetworkTask::for_tarball now splits the userinfo off the request
URL and sends it as Authorization: Basic base64(user:pass), the header
npm sends for such URLs. Credentials configured for the registry scope
still take precedence when they apply to the tarball. The request URL no
longer carries the userinfo, so the HTTP client's same-origin check keeps
the header across a redirect within the host and error output prints the
URL without the credentials.
@coderabbitai

coderabbitai Bot commented Aug 15, 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: 16 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: 634e1f05-7d46-477e-a7ca-d42e94fcf8ac

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6398 and 25f7523.

📒 Files selected for processing (3)
  • docs/pm/cli/add.mdx
  • src/install/NetworkTask.rs
  • test/cli/install/bun-install.test.ts

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed, waiting for CI.

  • Reproduced on bun 1.4.0-canary.1 (eabb96d) and main: a local server logging the Authorization header receives null for a http://carol:s3cret@127.0.0.1:PORT/cdn/no-deps-1.0.0.tgz dependency and the install fails with a 401; npm 11.16 sends Basic Y2Fyb2w6czNjcmV0 for the same package.json.
  • Fix in src/install/NetworkTask.rs (for_tarball); tests in test/cli/install/bun-install.test.ts, describe("credentials embedded in a tarball URL"): 10 of the 12 fail on the unfixed build and all 12 pass with it.

@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.

I reviewed this PR and didn't find any bugs. Because it changes how Authorization headers are derived and sent for tarball downloads (credential handling, precedence vs. registry auth, redirect behavior), a human look would still be worthwhile.

Checked split_url_userinfo against scoped paths, empty userinfo, and multi-@ authorities — bounding on first /?# then last @ handles them.
Checked the HeaderBuilder two-pass count/append stays balanced for the URL-auth branch (debug_assert_eq!(content.len, content.cap) holds; npm-auth-type is correctly omitted).
Checked lifetimes: userinfo borrows the old url_buf and is fully consumed into a fresh Vec before the box is reassigned.
Checked that registry-scope credentials still win (header_count == 0 gate) and cross-origin redirects still strip the header (URL is requested without userinfo, so the existing origin compare works).

Extended reasoning...

Overview

The PR makes NetworkTask::for_tarball in src/install/NetworkTask.rs extract the userinfo from a tarball URL, send it as Authorization: Basic base64(userinfo), and request the URL without the userinfo. Two new helpers (split_url_userinfo, basic_authorization_from_userinfo) plus ~25 lines of integration in for_tarball. It adds 12 subprocess tests in test/cli/install/bun-install.test.ts and a doc line in docs/pm/cli/add.mdx.

Security risks

This is credential-handling code. The risks I looked for: leaking credentials to the wrong host (guarded — the existing HTTP client strips Authorization on cross-origin redirects, and stripping userinfo from the request URL means the origin comparison now works correctly for same-host redirects too), leaking registry credentials to a URL-specified host (unchanged — the origin match against the registry scope still gates send_auth, and registry credentials take precedence over URL ones when both apply), and printing credentials in error output (improved — the 401 error line now shows the URL without userinfo). The credentials sent are only those the user explicitly wrote in the URL for that host, or that a registry embedded in its own dist.tarball (in which case the registry already knows them). No new leak surface identified.

Level of scrutiny

High. Even though the change is small and closely follows npm's documented behavior (with a transcript against npm 11.16), it decides when and how an Authorization header is attached. Per the approval guidelines, auth-sensitive code should have a human sign-off regardless of apparent correctness.

Other factors

The implementation is careful: split_url_userinfo bounds the authority at the first /?# before searching for the last @, so /@scope/pkg paths are not misread (tested). The HeaderBuilder count/append two-pass stays byte-exact for the new branch (only Authorization counted and appended; npm-auth-type omitted for URL creds). Borrow of the old url_buf via userinfo ends before reassignment. Tests use local servers, port: 0, tempDir, await using, and concurrent stdout/stderr/exited drains, and cover the userinfo variant matrix, both linkers, redirects (same-host keeps, cross-host drops), the scoped-path guard, error-message redaction, and both precedence directions with registry credentials. No CODEOWNERS entry covers these paths. No prior human review comments to address.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:28 AM PT - Aug 15th, 2026

@robobun, your commit 25f7523 has some failures in Build #98030 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 39025

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

bun-39025 --bun

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Folded into #38867.

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.

2 participants