Skip to content

CL-6496: Fix three redaction leaks and cause-chain data loss in error-sink - #231

Merged
TheGreatAxios merged 6 commits into
mainfrom
cl-6496-redaction
Aug 21, 2026
Merged

TheGreatAxios merged 6 commits into
mainfrom
cl-6496-redaction

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes four issues a reviewer found against packages/error-sink/src/redact.ts before the queued 141-call-site sweep onto reportError:

  1. OAuth callback URL query-param tokens leaked completely — ?access_token=SECRET&code=abc passed through untouched. Added a name-based key=value assignment pattern that redacts only the sensitive value, keeping the URL's host/path/param names readable.
  2. Raw secret strings in an array under a non-secret key leaked — array/object recursion in redactValue/redactRecord already existed structurally; the real gap was pattern coverage for secrets with no keyword prefix. Added a JWT-shape pattern (eyJ...eyJ...) that catches this case without a nearby keyword.
  3. token=/key= style prefixes in free text weren't matched — only Bearer/Authorization: were. Same assignment pattern as (1) covers this.
  4. .cause chains silently vanished — redactedCopyOf only copied .message/.stack. It now recurses into .cause, redacting each link, depth-capped (5) so a cyclic or unbounded chain can't blow up the logger.

Also fixed in passing: the provider-key pattern required a hyphen (sk-, ghp-, ...) but GitHub actually issues underscore-separated tokens (ghp_...), so those were silently missing too.

reportError's never-throws guarantee is unchanged and covered by the existing throwing-sink test.

Follow-up from peer review: code=/key= over-redaction

Peer review caught that the original code=/key= assignment match was too broad — it destroyed everyday non-secret shapes: an HTTP code=404, logfmt's code=DB_TIMEOUT retries=3, a cache key=user:1234:profile. Fixed by splitting the pattern:

  • Unambiguous names (token, access_token, secret, password, api_key, ...) still match anywhere as a bare assignment.
  • code/key are scoped to right after a literal ? or & — the URL query-string case that motivated this in the first place (an OAuth code, an API key passed as a param) — and are left alone everywhere else.
  • The value-capture character class is now bounded (token-shaped chars only) so it can't run past the intended value into unrelated trailing text (e.g. a stack frame's :12:5)).

Added red/green tests for all four over-redaction cases plus confirming the URL case still redacts, and updated the README's redaction-coverage section to describe the query-string-only scope.

Test plan

  • bun test in packages/error-sink — 25 pass (tests for all issues above, confirmed red before each fix, green after)
  • bun run typecheck in packages/error-sink
  • repo-root bun run lint / bun run check — skipped locally on this push (a concurrent full-repo check had exhausted local machine memory); left to CI, which runs the full check on GitHub's machines.

Covers three redaction gaps in packages/error-sink/src/redact.ts (OAuth
callback URL query-param tokens, raw secret strings in arrays under a
non-secret key, and token=/key= style assignments in free text) plus the
loss of an Error's .cause chain when reportError copies it.
redactText missed OAuth callback URL query-param values entirely, raw
secret strings with no keyword prefix inside arrays under a non-secret
key, and token=/key= style assignments outside a Bearer/Authorization
header. Added a name-based assignment pattern that redacts only the
value (keeping URLs and messages structurally readable) and a JWT-shape
pattern for prefix-less tokens; fixed the provider-key pattern to also
match GitHub's underscore-separated tokens.

reportError also silently dropped an Error's .cause chain when building
its redacted copy. redactedCopyOf now recurses into .cause, redacting
each link, capped at a fixed depth so a cyclic or unbounded chain can't
blow up the logger.
States plainly what redaction is and isn't heuristically able to catch,
and that new code should call reportError rather than a raw log.error
call (the existing raw call sites predate this package and aren't a
pattern to copy).
Peer review on PR #231 found the free-text code=/key= assignment match
over-redacted everyday non-secret shapes (an HTTP code=404, logfmt's
code=DB_TIMEOUT, a cache key=...). These tests pin the intended
boundary: code=/key= must survive untouched in free text but still get
redacted when they appear as a URL query param, and a token= value must
not swallow unrelated trailing text.
code and key are too ambiguous with everyday non-secret shapes to
redact wherever they appear as a bare assignment -- code=404, logfmt's
code=DB_TIMEOUT, and a cache key=user:1234:profile were all getting
destroyed. Split the assignment pattern in two: unambiguous names
(token, secret, password, api_key, ...) still match anywhere; code and
key only match right after a literal ? or & (the URL query-string
case that motivated this in the first place). Also bounds the value
capture to a token-shaped character class so it can't run past the
intended value into unrelated trailing text.
Peer review flagged the prior wording as broader than the actual
(now-fixed) behavior. States explicitly that code=/key= redaction is
scoped to a URL query string and is deliberately left alone everywhere
else, since those two names collide too often with ordinary status
codes and cache keys.
@TheGreatAxios
TheGreatAxios merged commit 4f05b9c into main Aug 21, 2026
2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6496-redaction branch August 25, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant