diff --git a/apps/extension/src/debug/__tests__/redact.test.ts b/apps/extension/src/debug/__tests__/redact.test.ts index c2668661e..6a1c4d353 100644 --- a/apps/extension/src/debug/__tests__/redact.test.ts +++ b/apps/extension/src/debug/__tests__/redact.test.ts @@ -2,9 +2,41 @@ import { describe, expect, it } from "vitest"; import { jsonPointer, parseJsonSource } from "../json-source"; -import { BODY_CHARS, redactBody, redactRequestUrl } from "../redact"; +import { BODY_CHARS, redactBody, redactHeaders, redactRequestUrl } from "../redact"; describe("source-preserving evidence redaction", () => { + it.each([ + "PRIVATE-TOKEN", + "x-goog-api-key", + "x-amz-security-token", + "x-access-token", + "X-Vault-Token", + "Ocp-Apim-Subscription-Key", + "x-client-secret", + ])("redacts the vendor credential header %s", (name) => { + expect(redactHeaders({ [name]: "BSK_SYNTHETIC_SECRET_7264" })).toEqual({ + [name.toLowerCase()]: "[redacted]", + }); + }); + + it("redacts vendor credential fields in JSON bodies", () => { + const text = + '{"private_token":"BSK_SYNTHETIC_SECRET_7264","x_goog_api_key":"BSK_SYNTHETIC_SECRET_7264","page":2}'; + expect(redactBody(text, "application/json")).toMatchObject({ + text: '{"private_token":"[redacted]","x_goog_api_key":"[redacted]","page":2}', + redacted: true, + }); + }); + + it.each([ + "content-type", + "x-request-id", + "idempotency-key", + "x-tokenizer", + ])("keeps the ordinary header %s", (name) => { + expect(redactHeaders({ [name]: "value-7264" })).toEqual({ [name]: "value-7264" }); + }); + it.each([ 'type="password"', 'type="password"', diff --git a/apps/extension/src/debug/redact.ts b/apps/extension/src/debug/redact.ts index 98f0382b7..960d030e1 100644 --- a/apps/extension/src/debug/redact.ts +++ b/apps/extension/src/debug/redact.ts @@ -7,11 +7,25 @@ const MASK = "[redacted]"; export const BODY_CHARS = 64 * 1024; export const URL_CHARS = 2048; +/** + * Vendor credential names that name their secret in their last part: GitLab `private-token` (and the + * `private_token` query parameter), Google `x-goog-api-key`, AWS `x-amz-security-token`, Azure + * `ocp-apim-subscription-key`, Vault `x-vault-token`. + */ +const SECRET_HEADER_SUFFIX = + /(?:^|[_-])(?:token|api[_-]?key|apikey|secret|subscription[_-]key|password|passwd)$/i; + +function secretHeader(name: string): boolean { + return SECRET.test(name) || SECRET_HEADER_SUFFIX.test(name); +} + function secretField(key: string): boolean { return key .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .split(/[.[\]]+/) - .some((part) => SECRET.test(part) || /(?:^|[_-])(?:password|passwd|pwd)(?:[_-]|$)/i.test(part)); + .some( + (part) => secretHeader(part) || /(?:^|[_-])(?:password|passwd|pwd)(?:[_-]|$)/i.test(part), + ); } const HTML_FIELD_SEPARATORS: Record = { @@ -97,7 +111,7 @@ export function redactHeaderEvidence(value: unknown): { let remaining = 4 * 1024; for (const [key, raw] of entries.slice(0, 80)) { const name = key.slice(0, 128).toLowerCase(); - const secret = SECRET.test(name); + const secret = secretHeader(name); const original = String(raw); const redacted = secret ? MASK : redactText(original, 2048); const limit = Math.max(0, Math.min(2048, remaining - name.length));