Skip to content

[CLAUDE ROUTINE]: Security enhancement — let redactReason's Bearer-token regex span / so JWTs in queue payloads are fully redacted, not just the prefix #274

@NiveditJain

Description

@NiveditJain

Summary

redactReason is one of the last things that runs on a hook activity entry before it is enqueued for the relay. It scrubs known credential shapes out of the free-form reason string so the server only ever sees [REDACTED-…]. The Bearer-token branch is doing the right job for plain opaque tokens, but its character class stops short of /, which is a valid character in standard base64 — and JWTs in the wild use standard base64 (with / and +), not just base64url. As a result, a Bearer line containing a JWT gets redacted up to the first / and the rest is sent verbatim. A small character-class addition closes the gap.

Where

src/relay/queue.ts:73-81

function redactReason(reason: string | null | undefined): string | null {
  if (!reason) return reason ?? null;
  return reason
    .replace(/AKIA[0-9A-Z]{16}/g, "[REDACTED-AWS-KEY]")
    .replace(/eyJ[A-Za-z0-9_=-]+\.[A-Za-z0-9_=-]+\.[A-Za-z0-9_=-]+/g, "[REDACTED-JWT]")
    .replace(/ghp_[A-Za-z0-9]{36,}/g, "[REDACTED-GH-TOKEN]")
    .replace(/sk-[A-Za-z0-9]{20,}/g, "[REDACTED-API-KEY]")
    .replace(/Bearer\s+[A-Za-z0-9_.=+-]+/gi, "Bearer [REDACTED]");
//                              ^^^^^^^^^^^^^^^^^^
//                              missing `/` (and arguably `:` for some opaque tokens)
}

Two interacting things make this matter:

  1. The standalone JWT regex on the line above only matches a JWT when the token is in the canonical eyJ... shape AND uses base64url (_ / -). Many real-world Authorization: Bearer ... lines wrap a non-eyJ-prefixed opaque token, or a JWT whose middle/signature segment uses the + / / flavour of base64.
  2. The Bearer regex is the only thing that catches the long opaque case — and it stops at the first /.

Why this matters

flowchart LR
    Reason["reason = 'request failed: Authorization: Bearer ABC.def/ghi+jkl'"]
    Reason --> AWS{AKIA?}
    AWS -- no --> JWT{eyJ.. / .. / ..?}
    JWT -- no --> GH{ghp_?}
    GH -- no --> SK{sk-?}
    SK -- no --> Bearer["Bearer + chars in [A-Za-z0-9_.=+-]"]
    Bearer --> Out["Bearer [REDACTED]/ghi+jkl ← tail leaks"]
    style Out fill:#ffd6d6,stroke:#a00000
Loading

Concrete example:

Input reason (excerpt) Redacted output
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOi.../sIgnAtUre== Authorization: [REDACTED-JWT] ✓ (caught by the JWT branch)
Authorization: Bearer abc123/xyz789== Authorization: Bearer [REDACTED]/xyz789== ✗ (tail leaks)
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9 (no signature, opaque-looking) Authorization: Bearer [REDACTED]

The last two rows are the failure mode. They are particularly common with:

  • Server-issued opaque session tokens (base64(/+=) flavour).
  • Custom JWT-ish tokens whose signature segment includes raw / or +.
  • Tokens followed by a query-style suffix in the same word (Bearer xyz/foo).

In every case, the token prefix is redacted and the remainder is sent to PostHog / the relay server.

Suggested approach

A two-character change to the Bearer regex closes the leak:

// before
.replace(/Bearer\s+[A-Za-z0-9_.=+-]+/gi, "Bearer [REDACTED]");

// after — include `/` (and arguably `:` for token flavours that embed it)
.replace(/Bearer\s+[A-Za-z0-9_./=+:-]+/gi, "Bearer [REDACTED]");

Optional polish:

  • Use a length floor ({20,}) so we don't redact the literal word "Bearer foo" in unrelated prose.
  • Add a regression test in __tests__/relay/queue.test.ts (creating one if missing) for each of the three rows above.
  • While here, the JWT branch's [A-Za-z0-9_=-] could similarly accept / and + for non-base64url JWTs — same one-character fix.

Customer impact

  • Strictly better redaction: every existing redaction still fires, plus tokens that contain / (and friends) are now fully scrubbed.
  • No false positives on user-facing copy: the regex still requires the literal Bearer prefix.
  • Strengthens the redaction promise from "covers the common case" to "covers the common case AND the most common variant of the same token format" — a win for users worried about telemetry hygiene.

Acceptance criteria

  • redactReason("... Bearer abc/xyz==") returns "... Bearer [REDACTED]" with no trailing leak.
  • redactReason("... Bearer eyJ... / sig+pad ==") is fully redacted.
  • Existing tests (if any) for the Bearer branch keep passing.
  • New regression tests added.

Related

https://claude.ai/code/session_01SAwaAnE9bTuLujnvksoCYN

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions