Skip to content

logger: mask secrets in the source line before the excerpt window - #43316

Open
robobun wants to merge 7 commits into
mainfrom
robobun/bc99a221/keep-redacted-line-whole
Open

robobun wants to merge 7 commits into
mainfrom
robobun/bc99a221/keep-redacted-line-whole

Conversation

@robobun

@robobun robobun commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A syntax error in bunfig.toml prints part of a secret when the line is longer than about 120 bytes. For the repro in A syntax error in bunfig.toml or .npmrc can print part of a secret when the line is longer than 120 bytes #43311, bun install prints 2 | ETSECRETSECRET...SECRET" ] # xxx under error: Expected a newline or end of file after a key/value pair.
  • The cause is the excerpt window in Location::init_or_null_impl (src/ast/lib.rs:812). It keeps 40 bytes before the error and 80 after it, when the diagnostic is created. The redaction from Redact secrets in bunfig.toml and npmrc logs #14919 runs later, in the printer, and finds a secret by the key in front of it (token, _authToken, _auth, password, _password, email). When the key is more than 40 bytes left of the error, the window drops it, and the printer sees a plain string.

Fix

  • For a message with redact_sensitive_information set, init_or_null_impl runs bun_core::fmt::redacted_source over the whole line before the window. The window itself stays the same for every diagnostic. redacted_source becomes pub.
  • Correct because the masking sees the key, no matter where the error is, and a masked span keeps its byte length, so the window bounds and the caret do not move. line_text stays bounded to about 120 bytes, and BuildMessage.position.lineText for a config error now holds a masked line. The printer still runs its own redaction on the excerpt.
  • One visible change: with colors on, a numeric secret (token = 12345) now prints as ****** (one star per byte), the same as without colors. Before it printed a yellow ***.
  • Verified: test/cli/install/redacted-config-logs.test.ts (two new cases, one per logger entry point, both fail on 1.4.3). Also parse-error-column.test.ts, npmrc.test.ts, bun-run-bunfig.test.ts on the debug build.

Background

  • A Location carries a copy of its line's text, taken when the diagnostic is created. The printer (Data::write_format) draws N | <text> and a caret under it without a second look at the source.
  • redact_sensitive_information is a flag on a Msg. The TOML parser sets it for bunfig.toml, the INI parser for .npmrc. The two logger entry points that carry it, add_formatted_msg and add_error_opts, now pass it to init_or_null_impl.
  • logger: bound the excerpt printed for an error on a long line #43313 bounds what the printer writes for a long line and exempts a redacted line from that bound. With this PR the exemption is no longer needed. Both PRs add a test to the same file, so the second to land needs a small rebase. logger: indent the caret relative to the windowed line excerpt #41658 changes the window code next to this change and also needs a rebase.
Notes

#43311 was filed during the work on #43313, which found the cut while it bounded the printed excerpt.

Repro:

mkdir leak && cd leak && echo '{}' > package.json
node -e 'require("fs").writeFileSync("bunfig.toml", "[install]\ntoken = \"" + "SECRET".repeat(12) + "\" ] # " + "x".repeat(120) + "\n")'
NO_COLOR=1 bun install

Before (1.4.3): the last 38 bytes of the token print. After: 2 | **************************************" ] # xxx... with the caret at column 84. The excerpt still starts inside the value, but the value is masked.

A first revision kept the whole line for a redacted message instead of the window. Review preferred masking at creation: the window stays unconditional, line_text never holds the plaintext, and #43313 can drop its print-side exemption.

.npmrc: INI errors are placed at the value, so the 40 bytes before the error include the key. No .npmrc leak of this kind was found. The same code path is fixed.

Not covered here:

Suites run with the debug build: test/cli/install/redacted-config-logs.test.ts (18 pass), test/js/bun/transpiler/parse-error-column.test.ts (7 pass), test/cli/install/npmrc.test.ts (47 pass), test/cli/install/bun-run-bunfig.test.ts (28 pass).

A long line in bunfig.toml or .npmrc is cut to about 120 bytes around
the error when the diagnostic is created. The printer redacts a secret
by the key in front of it, so a cut that drops the key prints the value.

Keep the whole line when the message has redact_sensitive_information
set. Other diagnostics keep the window.
@coderabbitai

coderabbitai Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Warning

Review paused — included plan limit reached

Keep your review moving with free on-demand reviews.

  • Run this review for free

On-demand reviews are free for the next 2 days.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Promotion and pricing details

On-demand reviews are free for the next 2 days. After that, they cost $0.25 per reviewed file.

Review limit details

Or wait 3 minutes for your next included review.

Check out review usage here.

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 45556c50-05b8-434d-ba3a-bdbf4330abcb

📥 Commits

Reviewing files that changed from the base of the PR and between 367d939 and 1acbdcb.

📒 Files selected for processing (3)
  • src/ast/lib.rs
  • src/bun_core/fmt.rs
  • test/cli/install/redacted-config-logs.test.ts

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

LGTM — small, well-scoped fix: redacted diagnostics now keep the whole source line so the key-based masking in the printer can still see token = when the error lands far to its right, and the new test exercises both the color and no-color output paths.

Extended reasoning...

Overview

The diff touches src/ast/lib.rs (+~45 lines) and adds one test in test/cli/install/redacted-config-logs.test.ts. A private LineText { Windowed, Whole } enum is threaded through Location::init_or_null_tracked / init_or_null_impl and a new Log::tracked_range_data_with; the only behavioral change is that the ~120-byte excerpt window is skipped when line_text == Whole, which is selected via LineText::for_msg(redact_sensitive_information) at the two message-construction sites that carry the redact flag (add_formatted_msg, which backs add_error_fmt_opts, and add_error_opts). The old tracked_range_data delegates with Windowed, so every other caller is byte-for-byte unchanged.

Security risks

This is a data-exposure fix, not a new exposure surface. I grepped every site that sets redact_sensitive_information: true (src/ini/lib.rs, src/bunfig/bunfig.rs, src/install_types/NodeLinker.rs, src/parsers/toml.rs) — all of them go through add_error_opts or add_error_fmt_opts, both of which now pick Whole. The printer path (Data::write_format -> fmt_javascript / redacted_source) is untouched; it already redacts quoted values keyed on the preceding identifier, and in colored mode writes the quote, the * run, and the closing quote contiguously, so the test's "****..." assertion holds in both env variants. The only cost is that line_text for a redacted message is now an owned copy of a full config-file line instead of a bounded ~120 bytes; memory_cost already sums the actual line_text length so nothing else relied on the bound.

Level of scrutiny

Moderate: the change is small and mechanical, the new parameter is plumbed through a closed set of private functions, and the else branch in tracked_range_data_with that ignores line_text is harmless because init_or_null(None, ..) returns None with no line text at all. The caret indent in the printer is column - 1, which is exactly right for an un-windowed line, so no alignment regression is introduced for the Whole case. The changed files are not covered by .github/CODEOWNERS.

Other factors

The added test spawns bun install against a bunfig whose key sits more than 40 bytes before the error with more than 80 bytes of trailing comment, i.e. precisely the shape that defeats the old window, and asserts the secret is absent while the masked value and the parse error are present, under both NO_COLOR=1 and FORCE_COLOR=1. It follows the sibling test's structure, uses test.concurrent, tempDir, spread bunEnv, drains pipes concurrently, and asserts output before exit code. The single inline finding is a style nit ("*".repeat in the expected-mask string) with no functional bearing.

Comment thread test/cli/install/redacted-config-logs.test.ts Outdated
@robobun

robobun commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:28 PM PT - Sep 18th, 2026

✅ @robobun, your commit 1acbdcb023a6983b67f29be64fe892cef5fc765b passed in Build #117876! 🎉


🧪   To try this PR locally:

bunx bun-pr 43316

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

bun-43316 --bun

Comment thread src/ast/lib.rs Outdated
Comment thread src/ast/lib.rs Outdated

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked caret alignment for Whole lines — Data::write_format indents by column - 1 from the line start, so an untrimmed line is the case where the caret lands exactly right. I also looked at whether an unbounded line_text on the redaction paths could blow up output: it is only reachable from bunfig.toml/.npmrc diagnostics and the printer in this checkout writes the line as-is, so the cost is one config line per message.

Extended reasoning...

The diff threads a LineText mode through Location::init_or_null_impl and picks Whole on the two add_* paths that carry redact_sensitive_information. The stored column was never adjusted for the 40-byte left trim, so the printer's column - 1 indent is correct for a whole line and (pre-existing) off for a windowed one; this PR does not make that worse. The Whole lines are owned Vec<u8> copies of a single line from a local config file, only materialized on error paths, which is not a meaningful memory or output concern. The prior nit about "*".repeat was addressed in the follow-up commit (the test now uses Buffer.alloc(secret.length, "*")).

Comment thread test/cli/install/redacted-config-logs.test.ts Outdated

@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 — both points from the earlier passes are addressed in 981ec74 (the masked literal is built with Buffer.alloc, and a second case now exercises Log::add_error_opts), and the fix is small and self-contained.

What was reviewed:

  • Every site in src/ that sets redact_sensitive_information: true (bunfig, ini/.npmrc, NodeLinker, fmt_jsc) reaches the logger through either add_error_fmt_opts -> add_formatted_msg or add_error_opts; both now pass LineText::for_msg(...), so no redacting path still gets the 120-byte window.
  • init_or_null_tracked/tracked_range_data are private to src/ast/lib.rs; the external Location::init_or_null callers (parser, lockfile, linker) never carry the redaction flag, so keeping them on Windowed is correct.
  • In Whole mode column is the untrimmed column and write_format indents the caret by column - 1, so caret alignment is unchanged for whole lines.
  • The new test asserts the secret is absent and the masked literal present under both NO_COLOR and FORCE_COLOR, covering the redacted_source and fmt_javascript printer branches.
Extended reasoning...

Overview

The PR touches one Rust file (src/ast/lib.rs) and one test file. It adds a private two-variant enum LineText { Windowed, Whole }, threads it through Location::init_or_null_tracked -> init_or_null_impl and a new Log::tracked_range_data_with, and gates the existing ~120-byte line-window on Windowed. The two logger entry points that carry redact_sensitive_information (add_formatted_msg, reached via add_error_fmt_opts, and add_error_opts) select Whole when the flag is set. All other tracked_range_data callers delegate with Windowed, so their behavior is unchanged. The removed comment about full_line being bounded is accurate to delete since Whole mode now copies the full line.

Security risks

This is a secret-exposure fix: previously, when a bunfig.toml/.npmrc key like token = fell outside the 40-byte lookbehind window, the redaction highlighter could not identify the string as sensitive and printed it in clear. Keeping the whole line makes the redaction strictly more complete; there is no path on which the new code prints more of a secret than before. I grepped every redact_sensitive_information: true site in src/ (bunfig.rs, ini/lib.rs, install_types/NodeLinker.rs, jsc/fmt_jsc.rs) and each goes through one of the two updated entry points, so the class is covered rather than only the reported repro. The one residual consideration is that a redacted line is no longer bounded to ~120 bytes at creation time; the PR description says the printer side bounds output separately, and in practice these are hand-written config lines, so this is not a meaningful memory or DoS concern.

Level of scrutiny

Moderate. The diff is small and mechanical, but it is a refactor of a shared diagnostic helper, so I checked that the Windowed path is byte-for-byte the old behavior (it is: same condition, same lo/hi computation, same UTF-8 boundary adjustment) and that Whole mode does not desynchronize the caret. column comes from data.column_count regardless of windowing, and write_format indents by column - 1, which is correct for an untrimmed line. The signature changes are all to private functions within src/ast/lib.rs, so there are no stale external call sites.

Other factors

Both nits I raised on earlier pushes are addressed in the latest commit: the expected mask now uses Buffer.alloc, and a second test case (install = { registry = { token = ... }, cafile = 1, ... }) reaches Log::add_error_opts, so reverting either LineText::for_msg arm would now fail a test. The test runs under both NO_COLOR and FORCE_COLOR, drains stdout/stderr/exited concurrently, asserts output before exit code, and uses tempDir/await using per harness conventions. The bug hunt exited on dry_streak with no findings, and there are no outstanding third-party objections in the timeline (the github-actions inline comments were followed by commits from the author). I did not run the debug build for this run; the approval rests on reading the diff and tracing every redaction site.

Keep the window for every diagnostic. For a message with
redact_sensitive_information set, run redacted_source over the whole
line first, so the key in front of a secret is seen before the window
can cut it away. The masked line has the same byte length, so the
window bounds and the caret do not move.
Comment thread src/ast/lib.rs Outdated
@robobun robobun changed the title logger: keep the whole source line for a redacted diagnostic logger: mask secrets in the source line before the excerpt window Sep 18, 2026

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I checked the mask-before-window shape now in init_or_null_impl: redacted_source emits one * per masked byte, so offset_in_line computed on the unmasked source.contents still lines up with the masked buffer and the caret, and the * bytes are ASCII so the UTF-8 boundary loops are unaffected. Every redact_sensitive_information: true producer (bunfig, ini, NodeLinker) routes through add_error_opts or add_error_fmt_opts, both of which now thread the flag. One note: the PR description still describes a LineText { Windowed, Whole } enum and an unbounded line_text, but the code masks the full line and then applies the existing ~120-byte window, so the description is stale.

Extended reasoning...

The current diff replaces the earlier enum approach with a bool threaded through Location::init_or_null_tracked / init_or_null_impl and a new Log::tracked_range_data_with; when set, the whole source line is masked via bun_core::fmt::redacted_source into an owned Cow before the 40/80-byte window is applied. I verified the masking is length-preserving (splat_byte_all(b'*', len) for exactly the secret's byte span, write_bytes for everything else), so the window offsets and column - 1 caret indent computed from the unmasked source remain valid, and the is_utf8_char_boundary adjustments operate on ASCII * inside masked spans. All producers that set the flag reach one of the two updated entry points, so no sibling path still windows an unmasked line. The test covers both entry points (TOML syntax error via add_formatted_msg, bunfig validation error via add_error_opts) under NO_COLOR and FORCE_COLOR with exact excerpt regexes. The remaining inline finding (multi-line TOML strings not matched by starts_with_redacted_item) is pre-existing and security-relevant, so a human should weigh whether it belongs in this PR; the description also no longer matches the implementation.

Comment thread src/ast/lib.rs

This branch has not been deployed

No deployments
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