Skip to content

install: redact secrets in the URLs printed for failed manifest and tarball downloads - #38817

Open
robobun wants to merge 1 commit into
mainfrom
farm/2b0790fc/redact-install-error-urls
Open

install: redact secrets in the URLs printed for failed manifest and tarball downloads#38817
robobun wants to merge 1 commit into
mainfrom
farm/2b0790fc/redact-install-error-urls

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • When a manifest or tarball request fails with an HTTP error, bun install prints the URL of that request verbatim, including any secret inside it:
    • error: GET http://alice:s3cret@127.0.0.1:PORT/no-deps - 404
    • error: GET http://127.0.0.1:PORT/cdn/pkg-1.0.0.tgz?token=npm_... - 404
    • error: failed to download pkg@1.0.0: 404 Not Found followed by the same unredacted URL (isolated linker)
  • These URLs come from the registry, not from the user's own files: the manifest URL as finally requested (a redirect target when the registry redirected), or the dist.tarball URL out of the manifest (also what bun.lock records for an npm package whose tarball is not at the default location). Registries and CDNs do hand out URLs with a token or password in them, so the user may never have seen the secret that ends up in their CI log. Credentials written into the configured registry URL itself used to end up here too; install: send credentials embedded in --registry and registry env var URLs #38796 (merged) moves those into the Authorization header, which removes that one source but not the other two, which still print on current main.
  • Cause: the four GET {} - {} lines in src/install/PackageManager/runTasks.rs (manifest and tarball, error and warning variants) and the two failed to download lines in src/install/isolated_install/Installer.rs format the URL with bstr::BStr::new. Robustness pass across install, css, ffi, crypto, spawn, shell, and node compat #36165 routed the bun publish / bun pm view error path and the verbose request line through redacted_npm_url; these lines were missed.

Fix

Background

  • redacted_npm_url (src/bun_core/fmt.rs) is a Display adapter over URL bytes. It replaces the password part of scheme://user:password@host with one * per character, and any UUID or npm_/npms_ token anywhere in the string with ***; everything else is written through unchanged.
  • The install loop keeps two copies of a request URL: task.url_buf is what it asked for, metadata.url is the URL of the response it actually got (the redirect target when the registry redirected). The GET lines print the latter.
  • A package's resolution is how bun records where it came from: for a registry package it displays as the version (pkg@1.0.0), for a tarball or git dependency written in package.json it displays as that URL. Only the registry case is touched here, because there the URL is carried by the separate URL operand that this PR redacts.
  • With --linker isolated, tarballs still missing from the cache at install time are downloaded by the store installer; its failures (HTTP status, network error, extract failure) all go through on_package_download_error / on_task_fail and print the failed to download line instead of the GET line. The hoisted linker prints the GET line in both phases.

@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: 1 minute

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: 81962128-981d-4500-8c49-a2c175dfecec

📥 Commits

Reviewing files that changed from the base of the PR and between 732491c and dc39126.

📒 Files selected for processing (3)
  • src/install/PackageManager/runTasks.rs
  • src/install/isolated_install/Installer.rs
  • test/cli/install/redacted-config-logs.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 on the 1.4.0 release binary and on main, fix pushed (now a single commit, dc39126, rebased onto main after #38796 and #38183 landed), waiting on CI.

Repro used: bun install against a local Bun.serve that answers 404, with the secret placed (a) in the userinfo of the configured registry URL, (b) in a manifest redirect target, and (c) in the manifest's dist.tarball URL, with both the hoisted and the isolated linker. Before this change each variant printed the password / npm_ token on stderr; with it they print user:******@ / ?token=***. #38796 (merged) removes source (a) by moving those credentials into the Authorization header; (b) and (c) still leak on current main without this change (verified after the rebase: the five new tests in test/cli/install/redacted-config-logs.test.ts fail on main's source and pass with the six-line change; the whole file is 21 pass, 1 todo).

Scope: this PR covers the request URL operand of the six failure lines. A tarball or git URL the user wrote into package.json is still echoed verbatim by the lines that print the dependency's resolution (name@<url> failed to resolve, the name@<url> prefix of the isolated failed to download line, install summary, bun pm ls); that is a sweep over the same sites #38631 is touching and is being handled separately, on top of #38631. The todo test at the end of the new block records that shape. The four-line overlap with #38631 is described in the PR body and noted on #38631. The equivalent lines in bun audit were fixed on main by #38183.

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

LGTM — mechanical swap of bstr::BStr::new for the existing redacted_npm_url formatter at six print sites.

What was reviewed:

  • Confirmed redacted_npm_url(&[u8]) returns a Display adapter, so it drops in for BStr::new in the format-args position; URLs without secrets pass through byte-for-byte.
  • Checked audit_fix::mentions_package, which re-reads these log lines — it matches on /<name> in the path, unaffected by password/token redaction.
  • Tests cover all six sites (manifest error/warn, tarball error/warn, isolated linker), use a local server with port: 0, and assert both the redacted line and absence of the raw secret.
Extended reasoning...

Overview

Six one-line changes across src/install/PackageManager/runTasks.rs (four GET {} - {} lines) and src/install/isolated_install/Installer.rs (two failed to download lines), replacing bstr::BStr::new(url) with bun_core::fmt::redacted_npm_url(url) in error/warning format arguments. Five new tests in test/cli/install/redacted-config-logs.test.ts exercise each print site with a secret arriving via redirect or dist.tarball.

Security risks

None introduced — this reduces exposure by masking URL passwords, UUIDs, and npm_/npms_ tokens in log output. The formatter is already used for the same purpose in npm.rs, http/lib.rs, and bun pm whoami; this brings the install-loop lines into line.

Level of scrutiny

Low. The change is a formatter substitution in log-message arguments only; no control flow, allocation, or lifetime changes. redacted_npm_url has the same signature shape (&[u8] -> impl Display) as BStr::new, so the swap is type-equivalent at each site. I verified the one downstream consumer that parses these lines (audit_fix::mentions_package) matches on the package-name path segment, which the redactor never touches, and the PR description reports the relevant tests were re-run.

Other factors

Tests follow harness conventions: tempDir, bunEnv spread, local Bun.serve({port: 0}), concurrent pipe draining, describe.concurrent, await using for the server and process. Each test asserts the exact redacted line (toContain("...token=*** - 404")) plus not.toContain(password)/not.toContain(token) on both stdout and stderr, so a regression prints a useful diff rather than a bare boolean. The isolated-linker case seeds a lockfile with the secret-bearing tarball URL directly, hitting the on_task_fail path independently of the resolve-phase GET line.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:53 AM PT - Aug 15th, 2026

@robobun, your commit dc39126d4e02e411d3a9eac42da50cce8bfc5836 passed in Build #97900! 🎉


🧪   To try this PR locally:

bunx bun-pr 38817

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

bun-38817 --bun

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

LGTM — mechanical swap of bstr::BStr::new for the existing bun_core::fmt::redacted_npm_url formatter at six error-message sites.

What was reviewed

  • Confirmed redacted_npm_url takes &[u8] and impls Display, so it drops in for BStr::new at all six format-arg positions; same helper already used in npm.rs, http/lib.rs, and package_manager_command.rs.
  • Read the formatter impl — for URLs with no password/UUID/npm_ token it writes bytes through unchanged, so callers that parse the GET <url> - <status> line back out are unaffected.
  • Tests follow harness conventions (tempDir, port: 0, per-test cache dir, concurrent pipe drain, exit code asserted last) and cover each of the four runTasks.rs sites plus the isolated-linker on_task_fail path; the .todo documents the adjacent resolution-formatter case that is intentionally out of scope.
Extended reasoning...

Overview

The PR swaps bstr::BStr::new(url) for bun_core::fmt::redacted_npm_url(url) at six format-argument positions: four GET {} - {} lines in src/install/PackageManager/runTasks.rs (manifest error/warn, tarball error/warn) and two failed to download ... {url} lines in src/install/isolated_install/Installer.rs (on_package_download_error and the TaskError::Download arm of on_task_fail). Five new tests in test/cli/install/redacted-config-logs.test.ts exercise each print site with secrets that arrive via a manifest redirect target or dist.tarball, plus a .todo for the resolution-formatter case that is not addressed here.

Security risks

None introduced. The change is strictly security-positive: it stops bun install from echoing URL userinfo passwords and npm_/UUID tokens to stderr on 4xx/5xx download failures. redacted_npm_url is a pure Display adapter over the same &[u8] that was already being printed; no new parsing, allocation, or side effects.

Level of scrutiny

Low. This is a one-for-one formatter substitution at six call sites, using an in-tree helper that is already the established pattern for the same purpose (Npm::response_error in src/install/npm.rs:252, the verbose HTTP request line in src/http/lib.rs:1443, bun pm whoami in package_manager_command.rs:318). Both BStr::new and redacted_npm_url take &[u8] and return impl Display, so no signature or borrow changes ripple out. Reading the RedactedNpmUrlFormatter::fmt impl at src/bun_core/fmt.rs:233-278 confirms that a URL with no password/UUID/npm_ token is written through byte-for-byte, so any downstream code (the PR mentions bun audit fix/bun update) that pattern-matches these lines is unaffected for plain URLs.

Other factors

The tests are well-constructed per the repo's review guide: local Bun.serve({port:0}) registry, tempDir + isolated BUN_INSTALL_CACHE_DIR, await using for server/proc, Promise.all on stdout/stderr/exited, exact assertion on the redacted line (carol:******@.../...?token=***), not.toContain on the raw secret in both streams, exit code asserted last, describe.concurrent for speed. The .todo test transparently documents that a tarball URL written directly in package.json still leaks via the resolution formatter — a distinct print site the author scoped out. No CODEOWNERS entry covers src/install/. The robobun CI comment flags failures on the first commit (08f5201); a follow-up commit (d9f6ead) tightened the tests, and the six-line source change itself cannot plausibly cause unrelated failures.

Jarred-Sumner added a commit that referenced this pull request Aug 15, 2026
… registry URLs (#38183)

### Problem
- A registry configured as `http:host:port/path/` (scheme followed by a
single colon, which `new URL()` and npm accept) fails every resolution
before a request is made:
  ```
error: Invalid package name "react": manifest URL
"http://host:port/path/react" is not on registry "http:host:port/path/"
  error: InvalidURL
  ```
- Same failure for the other spellings the WHATWG parser rewrites (probe
against a local server on the released 1.4.0, full output in the details
below): a `..` segment in the path, an unencoded space in the path,
backslashes, surrounding whitespace. Three entries of the "Registry
URLs" table in `bun-install.test.ts` (`https:example.org`,
`https://////example.com///`, `http://點看`) hit it too; the table did not
notice because `failed to resolve` is also printed after this rejection.
- An upper-case scheme passes the manifest check (it compares
case-insensitively) but fails `for_tarball`'s same-origin comparison
(`src/install/NetworkTask.rs`, `send_auth`), which is case-sensitive, so
the tarball is requested without the `Authorization` header.
- Cause: `Scope::from_api` (`src/install/npm.rs`) and the `--registry`
branch of `Options::load`
(`src/install/PackageManager/PackageManagerOptions.rs`) store the
registry href as written. `NetworkTask::for_manifest` builds the
manifest URL with `bun_url::join`, which runs the href through the
WHATWG parser, and then compares the result against `URL::parse` of the
stored string. `URL::parse` is Bun's lenient scanner: it only recognizes
a scheme followed by `://`, does not resolve `..`, percent-encode, strip
whitespace or lower-case, so for these spellings the two sides describe
different URLs (`http:host:port/...` parses as hostname `http` with no
protocol). The same stored string feeds `for_tarball`'s origin
comparison, `extract_tarball::build_url`, `url_is_under_registry` in
`bun.lock.rs`, the DNS prefetch and the `@@<hostname>` cache folder
name.

### Fix
- Adds `Scope::set_url`: stores the WHATWG serialization of the
configured URL (`bun_url::URL::from_string`, the same parser `join`
uses) and derives `url_hash` from it. `Scope::from_api`, the
`--registry` branch and the `parseManifest` test helper
(`src/install_jsc/npm_jsc.rs`) all build the URL through it, so there is
one place that decides what a `Scope` holds.
- Correct because the stored href now equals the base `join` resolves
against, so every consumer that compares with, concatenates onto or
hashes the href agrees with the URL actually requested. The on-registry
check itself is unchanged and still rejects a name that joins outside
the registry directory (tested).
- Credentials cannot reach anything new. The manifest request URL is
unchanged (`join` parsed its base with the WHATWG parser before this
change too, so `join(as written, name)` and `join(normalized, name)` are
the same URL); only the value it is compared against changes. Both
checks compare against the stored href, so the only origin a tarball
request can now carry credentials to is the normalized registry origin,
which is where the manifest request (which always carries them) already
went. Before, a non-canonical spelling could only make the comparisons
fail. The one request URL that does change is the
`extract_tarball::build_url` fallback (manifest or `bun.lock` entry
without a tarball URL), which concatenates onto the href and now
produces a URL on that same origin instead of a string that failed the
`http(s)://` prefix check.
- A string the WHATWG parser rejects is stored as written, so the
`Failed to join registry "<as written>"` diagnostics for the invalid
entries of the table are unchanged (the table still asserts them).
- `set_url` runs after `from_api` has split the `/:_authToken=` style
credentials off the path, because the WHATWG parser would percent-encode
them. This is also why the normalization is not applied earlier, in the
config loaders: the `.npmrc` / bunfig string form extracts userinfo
credentials with the lenient parser first (an unencoded `#` in a
password is accepted there today).
- Already-canonical URLs serialize to themselves, so their `url_hash`,
manifest cache files and cache folder names are unchanged;
`https://registry.npmjs.org/` in particular still hashes to
`DEFAULT_URL_HASH`. The hash changes only for spellings the parser
rewrites; of those, only upper-case spellings worked before, and for
them the cost is one re-download of the cache.
- Not covered, on purpose: `.npmrc` credential lines
(`//host/path/:_authToken=`) are matched against the registry URL in
`src/ini/lib.rs` before a `Scope` exists, still by lenient parse of the
string as written, so a registry spelled `https:host/path/` gets its
requests but not its `.npmrc` token. That matching is being reworked in
#33869; filed separately. Spellings where the lenient parser takes the
port for a `:key=value` credential suffix (`http:/host:port/`,
`http:////host:port/`) are still mangled by the credential stripping
that runs before `set_url`; without a port they work.
- Verified: `test/cli/install/bun-install.test.ts` ("Registry URLs"):
new `spellings the WHATWG parser rewrites` block (bunfig registry object
with a token for each spelling, asserting the paths and `Authorization`
header of the manifest and tarball requests plus the cache folder name;
`.npmrc registry=`; `--registry`; the rejection message for a name that
joins outside the registry), and the table's handled entries now also
assert the rejection did not happen. 12 tests fail on the released build
(9 with the error above, the upper-case one with `authorization: null`
on the tarball, the rejection test because the message quoted the raw
spelling), all pass with this change.
- Also run with the change: the rest of `bun-install.test.ts` (remaining
failures are the bitbucket/gitlab/`some.url` network tests and
`--registry CLI flag`, which fail identically on the released build in
this container), `npmrc.test.ts`, the registry/whoami/manifest-cache
tests of `bun-install-registry.test.ts`,
`bun-install-pathname-trailing-slash.test.ts`, `cargo clippy` on
`bun_install` and `bun_install_jsc`, and the source lints.

### Background
- `npm::registry::Scope` is the package manager's record of one registry
(the default one or an `[install.scopes]` entry): its URL, credentials
and `url_hash`. `url_hash` keys the manifest cache files and tells
whether the default registry was overridden, which switches cache folder
names from `name@version` to `name@version@@<hostname>`.
- Bun has two URL parsers. `bun_url::URL::parse` is a lenient,
allocation-free scanner over the input bytes that the HTTP client and
the package manager use to read components out of a URL they already
hold. `bun_url::join` / `URL::from_string` call WTF::URL, the WHATWG
parser behind `new URL()`, which normalizes (scheme and host case,
missing slashes, `.`/`..` segments, percent-encoding, IDN) and rejects
what it cannot parse. The lenient scanner gives correct answers on
WHATWG output; the bug was feeding it the user's input instead.
- The "is not on registry" check exists so that a dependency name that
joins to another origin (an alias like `npm:\\other-host\pkg`) cannot
make Bun send the scope's credentials there; the tarball check in
`for_tarball` does the same for `dist.tarball` URLs returned by the
registry. Both are same-origin comparisons of a request URL against the
stored registry URL.

<details>
<summary>Probe: registry spellings against a local server (released
1.4.0 vs this branch)</summary>

Each row configures the spelling in `bunfig.toml` with one dependency
and records what reaches the server.

| registry as written | 1.4.0 | this branch |
| --- | --- | --- |
| `http://localhost:PORT/some/path/` | `GET /some/path/react` | same |
| `http:localhost:PORT/some/path/` | no request, `is not on registry` |
`GET /some/path/react` |
| `http:\\localhost:PORT\some\path\` | no request, `is not on registry`
| `GET /some/path/react` |
| `http://localhost:PORT/some/x/../path/` | no request, `is not on
registry` | `GET /some/path/react` |
| `http://localhost:PORT/some path/` | no request, `is not on registry`
| `GET /some%20path/react` |
| ` http://localhost:PORT/some/path/` (leading space) | no request, `is
not on registry` | `GET /some/path/react` |
| `HTTP://LOCALHOST:PORT/some/path/` | `GET /some/path/react` (tarball
would be sent without `Authorization`) | same request, tarball
authorized (covered by the test) |
| `http:/localhost:PORT/some/path/` | stored as `http://http/localhost/`
by the credential-suffix stripping | unchanged (see Fix) |
| `http:////localhost:PORT/some/path/` | stored as
`http://localhost/localhost/` by the credential-suffix stripping |
unchanged (see Fix) |

</details>

---

## Also folded in: audit — redact secrets in the registry URLs printed
by `bun audit` / `bun audit fix` (from #38844)

#### Problem
- With a registry URL that carries a secret, `bun audit` and `bun audit
fix` print it. With
`npm_config_registry=http://alice:s3cret@127.0.0.1:PORT/` and a registry
answering 404, stderr is `error: POST
http://alice:s3cret@127.0.0.1:PORT/-/npm/v1/security/advisories/bulk -
404`. A token in the registry path (`http://host/npm_.../`) is printed
the same way, and reaches these lines from every config source,
including `.npmrc`.
- Four outputs format the registry href verbatim (`BStr::new`), where
the rest of the package manager formats such URLs with
`bun_core::fmt::redacted_npm_url` (`Npm::response_error` in
`src/install/npm.rs`, the verbose request trace in `src/http/lib.rs`,
`bun pm whoami`):
- `src/runtime/cli/audit_command.rs` `send_audit_request`: the `POST
<url> - <status or error>` line (the repro above).
- `src/runtime/cli/audit_command.rs` `report_non_json_response`:
`<registry> returned a non-JSON audit response`, reached from the
report, `--json` and `audit fix` paths.
- `src/install/audit_fix.rs` `print_unaudited`: `warn: <registry> did
not answer the audit request (<reason>); skipped <packages>`, printed by
both commands for a scoped registry that failed.
- `src/install/audit_fix/json.rs`: the `registry` field of each
`unaudited` entry in `bun audit fix --json`.
- The href is `scope.url.href()` as configured
(`AuditRegistry::from_scope`; `unaudited()` copies it into the
`UnauditedRegistry` record behind the last two outputs). `.npmrc` and
bunfig registry strings move `user:password@` out of it while loading,
the bunfig object form, the registry env vars and `--registry` keep it
(#38796 and #38834 change the latter two), and a token in the path stays
in it in every case.
- The 1.3.x binaries print `audit request failed (status N)` without a
URL; the URL in these lines came with the audit rewrite in #38333, so
this has not shipped in a release.

#### Fix
- The `POST` line and `report_non_json_response` format the URL with
`redacted_npm_url`: these quote the request URL, so they get the same
masked form as the manifest and tarball error lines
(`http://alice:******@host/...`, path token as `***`).
`AuditRegistry.href` itself stays raw because it is also what the
request is sent to.
- The skipped-registry record names a registry rather than quoting a
request, so `unaudited()` builds it from `href_without_auth()` (trailing
slash stripped), the same form `bun publish` prints as its registry:
credentials written into the URL are left out instead of masked, and the
record reads the same whichever config source the scope came from
(`http://host:PORT`, matching what `.npmrc` scopes already produced).
Its two emitters, the warning and the `--json` field, format it with
`redacted_npm_url` for tokens in the path (`http://host:PORT/***`); the
`--json` value is rendered into a buffer first because the JSON string
writer takes bytes. The `unaudited` array is new in #38333, so nothing
depends on the raw form.
- For a URL without a password, UUID or `npm_` token the output is byte
for byte unchanged; the 177 existing tests in `bun-audit.test.ts`, many
of which assert these exact lines with plain URLs, still pass.
- Not touched, same class elsewhere: `bun audit fix`'s `was not checked
for updates` lines repeat the install log's `GET <url> - <status>` text,
which #38817 redacts at its source; the registry URL lines in
`src/install/NetworkTask.rs` (`Failed to join registry ...`, `... is not
on registry ...`) and `src/install/pnpm.rs` (`fetching pnpm registry ...
from <url>`) are install-side and have been filed separately. This PR
and #38817 touch disjoint files.
- Tests: `test/cli/install/bun-audit.test.ts`, new `bun audit with a
secret in the registry URL` block, one test per output: the `POST` line,
the non-JSON line from the response check, the non-JSON line from the
parse step in report, `--json` and `fix` mode, the skipped-registry
warning and `unaudited[].registry` with a token in the registry path
(`.npmrc` scope), and the same two outputs with `user:password@` in the
URL (bunfig object-form scope, which keeps it), asserting the
credential-free form. The path-token cases use a token rather than a
password because a token reaches the audit command from every config
source; the credential case asserts the stripped form, which stays true
once #38796 / #38834 strip credentials earlier, so this PR does not
depend on their landing order. All five fail on this branch with `src/`
stashed (the token or password is printed); the credential case also
fails with only the `unaudited()` hunk removed (it then prints
`alice:******@`), and the `--json` case with only the `json.rs` hunk
removed; all 182 tests in the file pass with the change.
- Also ran `cargo clippy -p bun_install -p bun_runtime`, `cargo fmt
--check` on both crates, and `test/internal/source-lints`.

#### Background
- `bun audit` POSTs the lockfile's package versions to
`<registry>/-/npm/v1/security/advisories/bulk`. Packages whose scope
(`@foo/*`) is configured with its own registry are sent to that registry
instead; when a non-default registry fails to answer (HTTP error,
connection error, non-JSON body) its packages are reported as skipped
(one `UnauditedRegistry` record per registry, printed as the warning
and, by `audit fix --json`, as the `unaudited` entries) rather than
failing the command, while a failure from the default registry fails the
command with the `POST` or non-JSON line.
- `redacted_npm_url` (`src/bun_core/fmt.rs`) is a `Display` adapter over
URL bytes: the password of `scheme://user:password@host` is written as
one `*` per byte (the per-byte form is shared with the config-excerpt
redactor, which needs column alignment), and any UUID or `npm_`/`npms_`
token anywhere in the string as `***`; everything else is written
through unchanged. `Output::err_generic`, `warn!` and `pretty_errorln!`
take any `Display` argument, so it is a drop-in replacement for
`BStr::new` (`err_generic`'s `{s}`/`{f}` placeholder letters are
cosmetic; `{f}` is what the other `redacted_npm_url` call site uses).
- `URL::href_without_auth()` (`src/url/lib.rs`) rebuilds
`scheme://host[:port]/path/` from the parsed URL, dropping any userinfo;
it is what the config loaders use to store a registry URL whose
credentials were split out, and what `bun publish` prints as
`Registry:`. It currently yields `http://host//` for a root-path
registry (#38812 changes that to one slash); `unaudited()` strips
trailing slashes afterwards, so the record is `http://host:PORT` either
way. Tokens that are part of the path survive it, which is why the
record's emitters still go through `redacted_npm_url`.

<details>
<summary>Before / after on a debug build (404 registry, non-JSON
registry, scoped registry with a path token, scoped registry configured
as <code>{ url = "http://alice:s3cret@..." }</code>)</summary>

Before:

```
error: POST http://alice:s3cret@127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8/-/npm/v1/security/advisories/bulk - 404
error: http://alice:s3cret@127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 returned a non-JSON audit response
warn: http://127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8","packages":["@foo/bar"],"reason":"404"}],...}
warn: http://alice:s3cret@localhost:PORT did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://alice:s3cret@localhost:PORT","packages":["@foo/bar"],"reason":"404"}],...}
```

After:

```
error: POST http://alice:******@127.0.0.1:PORT/***/-/npm/v1/security/advisories/bulk - 404
error: http://alice:******@127.0.0.1:PORT/*** returned a non-JSON audit response
warn: http://127.0.0.1:PORT/*** did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://127.0.0.1:PORT/***","packages":["@foo/bar"],"reason":"404"}],...}
warn: http://localhost:PORT did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://localhost:PORT","packages":["@foo/bar"],"reason":"404"}],...}
```

</details>

<details>
<summary>Earlier revision of this PR</summary>

The first revision applied `redacted_npm_url` to the `UnauditedRegistry`
record as well, so for the config sources that keep credentials in the
URL the warning and the `--json` field came out as
`http://alice:******@host` (username and password length, and a
different shape from the `.npmrc` case, which had no userinfo to begin
with). Review pointed out that every other place bun emits a registry
URL as data drops the credentials instead; the record is now built with
`href_without_auth()` and the per-byte masking is confined to the two
lines that quote the request URL.

</details>

Closes #38844

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · 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

<!-- robobun:evidence:end -->

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>

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

LGTM — mechanical swap of bstr::BStr::new for the existing redacted_npm_url formatter at six error-message sites, with a test per site.

What was reviewed:

  • Confirmed redacted_npm_url(&[u8]) matches the operand type at all six call sites and is already used for the same purpose in npm.rs, http/lib.rs, and package_manager_command.rs.
  • Checked that URLs without secrets pass through unchanged, so existing tests parsing the GET <url> - <status> line are unaffected.
  • Tests are hermetic (local Bun.serve, port: 0, per-test cache dir), cover error+warn variants and both linkers, and inject secrets via redirect/dist.tarball rather than the configured registry so they survive #38796.
Extended reasoning...

Overview

Six one-line changes replacing bstr::BStr::new(url) with bun_core::fmt::redacted_npm_url(url) in error/warning messages: four GET {} - {} lines in src/install/PackageManager/runTasks.rs (manifest error/warn, tarball error/warn) and two failed to download lines in src/install/isolated_install/Installer.rs. A new describe.concurrent block in test/cli/install/redacted-config-logs.test.ts adds five tests (one per print site) plus a .todo documenting the out-of-scope resolution-display case.

Security risks

None introduced. This is a security improvement — it stops bun install from printing registry-supplied credentials (userinfo passwords, npm_ tokens, UUIDs) verbatim to stderr on HTTP failures, which would otherwise land in CI logs. The formatter is passthrough for URLs without secrets.

Level of scrutiny

Low. The change is a pure Display-formatter substitution at log sites; it does not touch control flow, request handling, or the URL bytes themselves. redacted_npm_url is the established in-tree helper for exactly this and is already used at the sibling bun pm view/bun publish/verbose-HTTP sites (per #36165). The signature fn redacted_npm_url(&[u8]) -> RedactedNpmUrlFormatter<'_> matches every call site (metadata.url.slice(), url: &[u8], &dl.url).

Other factors

The tests follow harness conventions well: tempDir, bunEnv spread with a per-test BUN_INSTALL_CACHE_DIR, concurrent pipe draining, await using for the server and process, describe.concurrent for independent subprocess tests, exit code asserted last, and both negative (not.toContain(secret)) and positive (toContain(maskedLine)) assertions so the test can't pass vacuously. The secrets are injected via a manifest redirect and dist.tarball rather than the --registry URL, keeping the tests meaningful after #38796. The PR description explicitly enumerates the sibling sites intentionally left for the follow-up on top of #38631 and pins the remaining shape with a .todo test. No outstanding reviewer comments.

…arball downloads

The GET <url> - <status> lines in runTasks.rs and the isolated linker's
"failed to download" line formatted the URL verbatim, so a password in
the URL's userinfo or an npm_ token / UUID in its query ended up on
stderr. Format them with bun_core::fmt::redacted_npm_url, which the
publish / pm view error path and the verbose request line already use.

The tests feed the secret in through a manifest redirect and through
dist.tarball, so they cover the registry-supplied URLs this change is
about; the tarball spec a user writes into package.json is still echoed
by the resolution-printing lines and is recorded as a todo.

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

LGTM — mechanical swap of bstr::BStr::new for the existing bun_core::fmt::redacted_npm_url formatter at six error-message sites, with per-site test coverage.

What was reviewed:

  • Confirmed redacted_npm_url(&[u8]) -> impl Display is a drop-in replacement at all six format-arg positions; no control-flow or lifetime change.
  • Checked the other in-tree callers (npm.rs, http/lib.rs, audit_fix, package_manager_command) — this brings the install failure lines in line with the same helper they already use.
  • Tests: local Bun.serve registry, port: 0, tempDir, pipes drained concurrently, assert both the masked output and absence of the raw secret; secrets enter via redirect / dist.tarball so the tests stay valid after #38796.
Extended reasoning...

Overview

Six one-line edits across src/install/PackageManager/runTasks.rs (four GET {} - {} lines: manifest error/warning, tarball error/warning) and src/install/isolated_install/Installer.rs (two failed to download ... <url> lines), replacing bstr::BStr::new(url) with bun_core::fmt::redacted_npm_url(url) in the format-argument position. test/cli/install/redacted-config-logs.test.ts gains a describe.concurrent block with five tests (one per print site) plus a test.todo documenting the resolution-display sites that are intentionally out of scope pending #38631.

Security risks

None introduced; the change is strictly security-positive. It redacts userinfo passwords and npm_/UUID tokens from URLs that reach stderr on download failure — URLs that come from the registry (redirect targets, dist.tarball) rather than from the user's own config, so the user may never have seen the secret that would otherwise land in a CI log. redacted_npm_url passes non-secret URLs through byte-for-byte, so there is no information loss for the debuggable case and no behavior change for the bun audit fix / bun update code that parses these lines.

Level of scrutiny

Low. This is a formatter swap at print sites only — no control flow, allocation, or lifetime changes. The helper is the established in-tree redaction formatter already used by Npm::response_error, the verbose HTTP request line, bun pm whoami, and audit_fix; this PR just closes the gap left by #36165. Both operands (metadata.url.slice(), url, &dl.url) are &[u8], matching the helper's signature exactly.

Other factors

  • Tests follow the repo's harness conventions: local Bun.serve({ port: 0 }), tempDir, {...bunEnv, ...}, concurrent pipe drain, exit-code asserted last, and assert both presence of the masked string and absence of the raw password/token on both stdout and stderr. The secrets are injected via a 302 redirect and via dist.tarball (not via the configured --registry URL), so the tests remain meaningful once #38796 strips userinfo from the configured registry.
  • The variant matrix is covered: required vs optional dependency (error vs warn), manifest vs tarball, hoisted vs isolated linker. The test.todo with an explanatory comment records the resolution-display sweep that is deferred to land on top of #38631.
  • No CODEOWNERS coverage for the touched paths. No prior reviewer comments to address.

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.

1 participant