From aeb30464ac738ec232bd3ce22e0da18ae2aac9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 15 Jun 2026 01:28:19 +0200 Subject: [PATCH] chore(observability): add headerValue to the redact denylist The webhook shared secret lives only inside the AES-GCM-encrypted channel config blob and the GET masks it, so no current path excerpts it into a wide event. Add headervalue to the sensitive-key denylist as defence in depth: if a future change ever routes a webhook body through the payload diagnostic, the secret redacts instead of landing verbatim. The Live Activity push token is already covered by the token pattern. --- .../observability/__tests__/redact-payload.test.ts | 14 ++++++++++++++ src/lib/observability/redact-payload.ts | 7 +++++++ 2 files changed, 21 insertions(+) diff --git a/src/lib/observability/__tests__/redact-payload.test.ts b/src/lib/observability/__tests__/redact-payload.test.ts index 713f47897..0f9491efb 100644 --- a/src/lib/observability/__tests__/redact-payload.test.ts +++ b/src/lib/observability/__tests__/redact-payload.test.ts @@ -163,4 +163,18 @@ describe("redactSensitiveFields", () => { benignField: "kept", }); }); + + it("redacts the webhook shared secret (headerValue) and the Live Activity token", () => { + const body = { + headerValue: "super-secret-webhook-key", + liveActivityPushToken: "abc123", + webhookUrl: "https://example.com/hook", + }; + expect(redactSensitiveFields(body)).toEqual({ + headerValue: "[redacted]", + // Covered by the existing /token/i pattern. + liveActivityPushToken: "[redacted]", + webhookUrl: "https://example.com/hook", + }); + }); }); diff --git a/src/lib/observability/redact-payload.ts b/src/lib/observability/redact-payload.ts index 1ee5992e8..6b7a1f4a7 100644 --- a/src/lib/observability/redact-payload.ts +++ b/src/lib/observability/redact-payload.ts @@ -65,6 +65,13 @@ export const SENSITIVE_KEY_PATTERNS: readonly RegExp[] = [ /progesteronetest/i, /contraceptive/i, /cervicalmucus/i, + // v1.17.1 — the webhook shared secret. `headerValue` lives only inside the + // AES-GCM-encrypted `NotificationChannel.config` blob and the GET masks it + // to `hasHeaderValue`, so no current path excerpts it. Defence-in-depth: if + // a future change routes a webhook body through `buildPayloadDiagnostic`, + // the secret redacts instead of landing verbatim. The Live Activity push + // token is already covered by `/token/i`. + /headervalue/i, ]; const REDACTED = "[redacted]";