fix(logging): scrub every credential prefix the app mints

The log and error redactor missed the registration invite token (hlv_) and
the native sign-in handoff code (hlh_). The filter that keeps secrets out
of the idempotency replay cache and out of relayed upstream error bodies
missed the handoff code and the three MCP OAuth artifacts (hlac_, hlrt_,
hlc_). Both now cover every prefix the app issues: hlk_, hlr_, hls_, hle_,
hlv_, hlh_, hlac_, hlrt_ and hlc_.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 20:10:57 +02:00
1 parent d7b5fcd6c2
commit ba70e19cf9
4 files changed
+27 -6

No files matched your search

+4
View File
@@ -9,6 +9,10 @@ describe("looksSecretShaped", () => {
["a share-link token", "see hls_def456"],
["an invite token", "hlv_0a1b2c3d"],
["an elevation token", "hle_0a1b2c3d"],
["a native sign-in handoff code", "hlh_Ab9-xYz_12"],
["an MCP authorization code", "hlac_eyJ0eXAi"],
["an MCP refresh token", "hlrt_eyJ0eXAi"],
["an MCP client id", "hlc_eyJ0eXAi"],
["an OpenAI key", "echoed sk-1234567890"],
["an Anthropic key", "sk-ant-api03-xyz12345"],
])("matches %s", (_label, text) => {
+11
View File
@@ -117,6 +117,17 @@ describe("redactSecrets", () => {
).toBe('{"token": "[REDACTED]"}');
});
it.each([
["an invite token", `hlv_${"0a".repeat(32)}`],
["a session secret", `hls_${"ab".repeat(32)}`],
["an elevation", `hle_${"cd".repeat(32)}`],
["a native sign-in handoff code", `hlh_${"Ab9_-".repeat(9)}Ab`],
["an MCP refresh token", `hlrt_${"x".repeat(40)}`],
])("redacts %s wherever it appears", (_label, token) => {
expect(redactSecrets(`code=${token} next`)).not.toContain(token);
expect(redactSecrets(`value: ${token}`)).toBe("value: [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");
+7 -4
View File
@@ -84,18 +84,21 @@ export function redactSecrets(input: string): string {
// identifiers. Capture group 1 = the leading separator (preserved).
.replace(/(^|[^A-Za-z0-9])sk-(?:ant-)?[A-Za-z0-9_-]{8,}/g, "$1[REDACTED]")
// HealthLog hex-bodied credentials: `hlk_` access token, `hlr_` refresh
// token, `hls_` session cookie secret (v1.30.32), `hle_` step-up
// elevation (v1.30.34). Same boundary-aware shape as the `sk-` rule. The
// token, `hls_` session cookie secret (v1.30.32) and clinician share
// token, `hle_` step-up elevation (v1.30.34), `hlv_` registration invite.
// 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[kres]_[A-Fa-f0-9]{32,}/g, "$1[REDACTED]")
.replace(/(^|[^A-Za-z0-9])hl[kresv]_[A-Fa-f0-9]{32,}/g, "$1[REDACTED]")
// v1.22.0 — the MCP OAuth bridge artifacts: the authorization code
// (`hlac_`), refresh token (`hlrt_`), and DCR client id (`hlc_`). All
// are signed, base64url-bodied, and credential-bearing, so they are
// scrubbed at the egress boundary like the native token formats above.
// `hlh_` is the one-time native sign-in handoff code, base64url too; it
// rides a custom-scheme redirect and is exchanged for a token pair.
.replace(
/(^|[^A-Za-z0-9])hl(?:ac|rt|c)_[A-Za-z0-9_.-]{16,}/g,
/(^|[^A-Za-z0-9])hl(?:ac|rt|c|h)_[A-Za-z0-9_.-]{16,}/g,
"$1[REDACTED]",
)
// v1.7.0 — `insurance`/`insuranceNumber`/`kvnr` (German statutory-
+5 -2
View File
@@ -4,7 +4,10 @@
* such text.
*
* `hlk_` access tokens, `hlr_` refresh tokens, `hls_` clinician share-link
* tokens, `hlv_` registration invite tokens, `hle_` elevation tokens;
* tokens and session secrets, `hlv_` registration invite tokens, `hle_`
* elevation tokens, `hlh_` native sign-in handoff codes, and the MCP OAuth
* artifacts `hlac_` (authorization code), `hlrt_` (refresh token) and `hlc_`
* (client id);
* `sk-…` / `sk-ant-…` OpenAI and Anthropic keys, in the full token form
* only: a bare `sk-` substring ("task-id", "risk-management") is not a key,
* and matching it would refuse benign text.
@@ -14,7 +17,7 @@
* that knows the secrets in play must also refuse text containing them.
*/
const SECRET_SHAPED =
/(?:\b(?:hlk_|hlr_|hls_|hlv_|hle_)[A-Za-z0-9_-]+|\bsk-(?:ant-)?[A-Za-z0-9_-]{8,})/;
/(?:\b(?:hlk_|hlr_|hls_|hlv_|hle_|hlh_|hlac_|hlrt_|hlc_)[A-Za-z0-9_-]+|\bsk-(?:ant-)?[A-Za-z0-9_-]{8,})/;
export function looksSecretShaped(text: string): boolean {
return SECRET_SHAPED.test(text);