diff --git a/src/utils/mask-api-key.test.ts b/src/utils/mask-api-key.test.ts index 6d0a35d331bc..5ea859f7d2ef 100644 --- a/src/utils/mask-api-key.test.ts +++ b/src/utils/mask-api-key.test.ts @@ -18,4 +18,11 @@ describe("maskApiKey", () => { it("masks long values with first and last 8 chars", () => { expect(maskApiKey("1234567890abcdefghijklmnop")).toBe("12345678...ijklmnop"); // pragma: allowlist secret }); + + it("strips control characters before masking diagnostic output", () => { + expect(maskApiKey("abcd\nefghijklmnop")).toBe("ab...op"); + expect(maskApiKey("abcd\u0000efghijklmnop")).toBe("ab...op"); + expect(maskApiKey("abcd\u007f\u0085efghijklmnop")).toBe("ab...op"); + expect(maskApiKey("\u0000\n")).toBe("missing"); + }); }); diff --git a/src/utils/mask-api-key.ts b/src/utils/mask-api-key.ts index 69ff0e201d43..b3e495f62b72 100644 --- a/src/utils/mask-api-key.ts +++ b/src/utils/mask-api-key.ts @@ -1,6 +1,6 @@ /** Masks credential-like values for diagnostics while preserving enough prefix/suffix to identify them. */ export const maskApiKey = (value: string): string => { - const trimmed = value.trim(); + const trimmed = stripControlCharacters(value).trim(); if (!trimmed) { return "missing"; } @@ -12,3 +12,15 @@ export const maskApiKey = (value: string): string => { } return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`; }; + +function stripControlCharacters(value: string): string { + let out = ""; + for (const char of value) { + const code = char.charCodeAt(0); + const isControl = (code >= 0x00 && code <= 0x1f) || (code >= 0x7f && code <= 0x9f); + if (!isControl) { + out += char; + } + } + return out; +}