From f0cfd26d96342d368c63727c3cd1a7926f7a4505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 17:43:02 +0200 Subject: [PATCH] fix(security): redact standalone hlk_ / hlr_ native API tokens in logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bearer rule already covers `Authorization: Bearer hlk_…` headers, but standalone tokens in error bodies, JSON dumps, or raw debug strings were untouched. The idempotency replay-cache rejects bodies containing `hlk_` / `hlr_` (CLAUDE.md headless-client-API patterns) exactly so cached responses can never echo a token back; this commit adds the matching egress guard for log/error surfaces. Boundary-aware shape mirrors the v1.5 phase-2 sk- regex tightening so benign identifiers like `html_render` or short `hlk_short` strings are not over-matched. Closes H4 from .planning/phase-6-code-review-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/logging/__tests__/redact.test.ts | 26 ++++++++++++++++++++++++ src/lib/logging/redact.ts | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/src/lib/logging/__tests__/redact.test.ts b/src/lib/logging/__tests__/redact.test.ts index 45fcaf264..3c7e93829 100644 --- a/src/lib/logging/__tests__/redact.test.ts +++ b/src/lib/logging/__tests__/redact.test.ts @@ -85,6 +85,32 @@ describe("redactSecrets", () => { expect(redactSecrets(",sk-1234567890abcdef,")).toBe(",[REDACTED],"); }); + it("redacts standalone HealthLog native API tokens (hlk_ / hlr_)", () => { + // Start of string. + expect( + redactSecrets("hlk_abc123def4567890abcdef1234567890ABCDEF1234567890abcd"), + ).toBe("[REDACTED]"); + expect( + redactSecrets("hlr_abc123def4567890abcdef1234567890ABCDEF1234567890abcd"), + ).toBe("[REDACTED]"); + // Preceded by separator, leading separator preserved. + expect( + redactSecrets( + "refresh: hlr_abc123def4567890abcdef1234567890ABCDEF1234567890abcd end", + ), + ).toBe("refresh: [REDACTED] end"); + expect( + redactSecrets('{"token": "hlk_0123456789abcdef0123456789abcdef0123"}'), + ).toBe('{"token": "[REDACTED]"}'); + }); + + it("does NOT redact non-token strings starting with `hl`", () => { + expect(redactSecrets("html_render finished")).toBe("html_render finished"); + expect(redactSecrets("healthcheck ok")).toBe("healthcheck ok"); + // Too-short trailing hex — keep as-is. + expect(redactSecrets("hlk_short")).toBe("hlk_short"); + }); + it("handles multiple secrets in one string", () => { // Bearer matches `\S+` so it greedily consumes `hlk_x;` — that's // safe (over-redaction is fine when the alternative is leaking). diff --git a/src/lib/logging/redact.ts b/src/lib/logging/redact.ts index 714f42c4d..2fe981be2 100644 --- a/src/lib/logging/redact.ts +++ b/src/lib/logging/redact.ts @@ -36,6 +36,12 @@ export function redactSecrets(input: string): string { // tiny safety margin against `sk-1`-style false-positives in code // identifiers. Capture group 1 = the leading separator (preserved). .replace(/(^|[^A-Za-z0-9])sk-(?:ant-)?[A-Za-z0-9_-]{8,}/g, "$1[REDACTED]") + // HealthLog native API tokens (`hlk_<64hex>` access, `hlr_<64hex>` + // refresh). Same boundary-aware shape as the `sk-` rule. The + // idempotency replay-cache already rejects bodies containing + // these prefixes (CLAUDE.md headless-client-API patterns); this + // is the matching egress guard for log/error surfaces. + .replace(/(^|[^A-Za-z0-9])hl[kr]_[A-Fa-f0-9]{32,}/g, "$1[REDACTED]") .replace( /([?&])(secret|code|token|api[_-]?key)=[^&\s]+/gi, "$1$2=[REDACTED]",