From dd61f0df2ca666ccdaaf8770e431c0608619233b Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:10:46 -0700 Subject: [PATCH 1/5] fix(sdk): redact standalone keys and structured credentials --- sdk/typescript/src/errors.ts | 23 ++++++++---- sdk/typescript/tests-ts/errors.test.ts | 50 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 sdk/typescript/tests-ts/errors.test.ts diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index a8face06..ccb45fa0 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -3,17 +3,26 @@ import { formatUsd, type ScanCost } from "./cost.js"; /** Returns an error message with credential-shaped substrings redacted. */ export function redactedErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error); - const withoutPrivateKeys = message.replaceAll( - /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, - "$1[redacted]", - ); + const withoutPrivateKeys = message + .replaceAll( + /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, + "$1[redacted]", + ) + .replaceAll( + /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, + "[redacted]", + ); return redactQuotedCredentialValues(withoutPrivateKeys) + .replaceAll( + /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?=[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=)[^\r\n]+/giu, + "$1$2$3[redacted]", + ) .replaceAll( /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?!\[redacted\]|(?!key\s*=)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*[:=]\s*(?=[^=\s"',;}&\\\]]))[^\s"',;}&\\\]]+/giu, "$1$2$3[redacted]", ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, "$1[redacted]", ) .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_*=-]{8,}/gu, "[redacted]") @@ -25,14 +34,14 @@ export function redactedErrorMessage(error: unknown): string { ) .replaceAll(/((?:https?|ssh|git\+ssh):\/\/)[^\s/@]+@/giu, "$1[redacted]@") .replaceAll( - /((?:[?&]|%3F|%26)(?:(?!%3F|%26|%3D)(?:[A-Za-z0-9_.%-]|\[|\])){0,64}(?:api[_-]?key|access(?:[_-]|%5F|%2D)?key(?:(?:[_-]|%5F|%2D)?id)?|private(?:[_-]|%5F|%2D)?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:(?:[_-]|%5F|%2D)[A-Za-z0-9_.%-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_.%-]{0,48})?(?:\]|%5D)?(?:=|%3D))(?:(?!%26)[^&\s])+/giu, + /((?:[?&]|%3F|%26)(?:(?!%3F|%26|%3D)(?:[A-Za-z0-9_.%-]|\[|\])){0,64}(?:api(?:[_-]|%5F|%2D)?keys?|access(?:[_-]|%5F|%2D)?key(?:(?:[_-]|%5F|%2D)?id)?|private(?:[_-]|%5F|%2D)?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:(?:[_-]|%5F|%2D)[A-Za-z0-9_.%-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_.%-]{0,48})?(?:\]|%5D)?(?:=|%3D))(?:(?!%26)[^&\s])+/giu, "$1[redacted]", ); } function redactQuotedCredentialValues(message: string): string { const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?key|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credential|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; let output = ""; let consumed = 0; for ( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts new file mode 100644 index 00000000..4b165f18 --- /dev/null +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test"; +import { redactedErrorMessage } from "../src/errors.js"; + +describe("security error redaction", () => { + test("redacts standalone and truncated PEM private-key envelopes", () => { + expect( + redactedErrorMessage( + "provider failed: -----BEGIN PRIVATE KEY-----\nSYNTHETIC_PRIVATE_KEY\n-----END PRIVATE KEY----- safe=value", + ), + ).toBe("provider failed: [redacted] safe=value"); + expect( + redactedErrorMessage( + "provider failed: -----BEGIN RSA PRIVATE KEY-----\nSYNTHETIC_TRUNCATED_KEY", + ), + ).toBe("provider failed: [redacted]"); + }); + + test("redacts every parameter from structured authorization schemes", () => { + expect( + redactedErrorMessage( + 'Authorization: Digest username="example", response=SYNTHETIC_DIGEST_SECRET', + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage("auth=Custom response=SYNTHETIC_AUTH_SECRET"), + ).toBe("auth=Custom [redacted]"); + }); + + test("redacts encoded API-key names without consuming other parameters", () => { + for (const separator of ["%5F", "%2D"]) { + const value = `https://example.test/?api${separator}key%3DSYNTHETIC_API_KEY%26safe%3Dvisible`; + expect(redactedErrorMessage(value)).toBe( + `https://example.test/?api${separator}key%3D[redacted]%26safe%3Dvisible`, + ); + } + }); + + test("redacts plural credential assignments and quoted values", () => { + expect( + redactedErrorMessage( + "credentials=SYNTHETIC_CREDENTIAL clientCredentials=SYNTHETIC_CLIENT apiKeys=SYNTHETIC_KEYS", + ), + ).toBe( + "credentials=[redacted] clientCredentials=[redacted] apiKeys=[redacted]", + ); + expect( + redactedErrorMessage('{"credentials":"correct horse battery staple"}'), + ).toBe('{"credentials":"[redacted]"}'); + }); +}); From 552c382ae40f4474b248fa09eb84da53a6f8b772 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:14:10 -0700 Subject: [PATCH 2/5] fix(sdk): preserve diagnostics after structured auth headers --- sdk/typescript/src/errors.ts | 2 +- sdk/typescript/tests-ts/errors.test.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index ccb45fa0..c6178159 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -14,7 +14,7 @@ export function redactedErrorMessage(error: unknown): string { ); return redactQuotedCredentialValues(withoutPrivateKeys) .replaceAll( - /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)(?=[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=)[^\r\n]+/giu, + /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 4b165f18..7edaaa69 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -24,6 +24,11 @@ describe("security error redaction", () => { expect( redactedErrorMessage("auth=Custom response=SYNTHETIC_AUTH_SECRET"), ).toBe("auth=Custom [redacted]"); + expect( + redactedErrorMessage( + "Authorization: Custom key=SYNTHETIC_AUTH_SECRET https://example.test/safe", + ), + ).toBe("Authorization: Custom [redacted] https://example.test/safe"); }); test("redacts encoded API-key names without consuming other parameters", () => { From e8b9589bc3e4bbead1c76e4d47966747362b9460 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:24:52 -0700 Subject: [PATCH 3/5] fix(sdk): redact structured auth and credential collections --- sdk/typescript/src/errors.ts | 45 ++++++++++++++++++++++++-- sdk/typescript/tests-ts/errors.test.ts | 25 ++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index c6178159..a06a43ee 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -12,9 +12,11 @@ export function redactedErrorMessage(error: unknown): string { /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, "[redacted]", ); - return redactQuotedCredentialValues(withoutPrivateKeys) + return redactQuotedCredentialValues( + redactCredentialCollections(withoutPrivateKeys), + ) .replaceAll( - /(\b(?:authorization|auth)\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9._~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_][A-Za-z0-9_-]{0,64}\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+))*/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( @@ -39,6 +41,45 @@ export function redactedErrorMessage(error: unknown): string { ); } +function redactCredentialCollections(message: string): string { + const assignment = + /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; + let output = ""; + let consumed = 0; + for ( + let match = assignment.exec(message); + match !== null; + match = assignment.exec(message) + ) { + const closings = [match[2] === "[" ? "]" : "}"]; + let quote: string | undefined; + let position = assignment.lastIndex; + while (position < message.length && closings.length > 0) { + const character = message[position]!; + if (quote !== undefined) { + if (character === "\\") position += 1; + else if (character === quote) quote = undefined; + } else if (character === '"' || character === "'") { + quote = character; + } else if (character === "[") { + closings.push("]"); + } else if (character === "{") { + closings.push("}"); + } else if (character === closings.at(-1)) { + closings.pop(); + } + position += 1; + } + + const prefix = match[1]!; + const replacement = /:\s*$/u.test(prefix) ? '"[redacted]"' : "[redacted]"; + output += `${message.slice(consumed, match.index)}${prefix}${replacement}`; + consumed = closings.length === 0 ? position : message.length; + assignment.lastIndex = consumed; + } + return output + message.slice(consumed); +} + function redactQuotedCredentialValues(message: string): string { const assignment = /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)(\\*)(["'])/giu; diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 7edaaa69..e6ce41d5 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -29,6 +29,21 @@ describe("security error redaction", () => { "Authorization: Custom key=SYNTHETIC_AUTH_SECRET https://example.test/safe", ), ).toBe("Authorization: Custom [redacted] https://example.test/safe"); + expect( + redactedErrorMessage( + 'Authorization: Digest username="a\\"b", response="SYNTHETIC_DIGEST_SECRET"', + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage( + "Authorization: Digest username*=UTF-8''user, response=SYNTHETIC_DIGEST_SECRET", + ), + ).toBe("Authorization: Digest [redacted]"); + expect( + redactedErrorMessage( + 'client_authorization_value=Digest username="example", response="SYNTHETIC_DIGEST_SECRET"', + ), + ).toBe("client_authorization_value=Digest [redacted]"); }); test("redacts encoded API-key names without consuming other parameters", () => { @@ -51,5 +66,15 @@ describe("security error redaction", () => { expect( redactedErrorMessage('{"credentials":"correct horse battery staple"}'), ).toBe('{"credentials":"[redacted]"}'); + expect( + redactedErrorMessage( + "credentials=[alice:SYNTHETIC_ONE,bob:SYNTHETIC_TWO] safe=visible", + ), + ).toBe("credentials=[redacted] safe=visible"); + expect( + redactedErrorMessage( + '{"credentials":["SYNTHETIC_ONE","SYNTHETIC_TWO"],"safe":"visible"}', + ), + ).toBe('{"credentials":"[redacted]","safe":"visible"}'); }); }); From bf34227cb2aee2fe9467bf54e6e0b6fc6628a88e Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:37:02 -0700 Subject: [PATCH 4/5] fix(sdk): redact suffixed credential collections --- sdk/typescript/src/errors.ts | 2 +- sdk/typescript/tests-ts/errors.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index a06a43ee..85b0e5a0 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -43,7 +43,7 @@ export function redactedErrorMessage(error: unknown): string { function redactCredentialCollections(message: string): string { const assignment = - /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; + /(\b[A-Za-z0-9_-]{0,64}(?:credentials|api[_-]?keys)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\*["'])?\s*[:=]\s*)([\[{])/giu; let output = ""; let consumed = 0; for ( diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index e6ce41d5..686b44a3 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -76,5 +76,15 @@ describe("security error redaction", () => { '{"credentials":["SYNTHETIC_ONE","SYNTHETIC_TWO"],"safe":"visible"}', ), ).toBe('{"credentials":"[redacted]","safe":"visible"}'); + expect( + redactedErrorMessage( + 'credentials_value=["SYNTHETIC_ONE","SYNTHETIC_TWO"] safe=visible', + ), + ).toBe("credentials_value=[redacted] safe=visible"); + expect( + redactedErrorMessage( + '{"api_keys_data":[{"key":"SYNTHETIC_ONE"},{"key":"SYNTHETIC_TWO"}]}', + ), + ).toBe('{"api_keys_data":"[redacted]"}'); }); }); From c2d786be76254c158c329634c2e9925bc0bfe937 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Fri, 7 Aug 2026 23:53:08 -0700 Subject: [PATCH 5/5] fix(sdk): close credential redaction review edge cases --- sdk/typescript/src/errors.ts | 8 ++++---- sdk/typescript/tests-ts/errors.test.ts | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/sdk/typescript/src/errors.ts b/sdk/typescript/src/errors.ts index 85b0e5a0..4a550e3f 100644 --- a/sdk/typescript/src/errors.ts +++ b/sdk/typescript/src/errors.ts @@ -5,18 +5,18 @@ export function redactedErrorMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error); const withoutPrivateKeys = message .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----(?:\\?["'])?|$)/giu, + /(\b[A-Za-z0-9_-]{0,64}private[_-]?key(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)(?:\\?["'])?-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----(?:\\?["'])?|$)/giu, "$1[redacted]", ) .replaceAll( - /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/giu, + /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----|$)/giu, "[redacted]", ); return redactQuotedCredentialValues( redactCredentialCollections(withoutPrivateKeys), ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[A-Za-z_*][A-Za-z0-9_.*-]{0,64}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:authorization|auth)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*)([A-Za-z0-9][A-Za-z0-9.+~-]{0,63})((?:\s|%20|\+)+)[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+)(?:\s*,\s*[!#$%&'*+.^_`|~0-9A-Za-z-]{1,65}\s*=\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;]+))*/giu, "$1$2$3[redacted]", ) .replaceAll( @@ -24,7 +24,7 @@ export function redactedErrorMessage(error: unknown): string { "$1$2$3[redacted]", ) .replaceAll( - /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])[^\s"',;}&\\\]]+/giu, + /(\b[A-Za-z0-9_-]{0,64}(?:api[_-]?keys?|access[_-]?key(?:[_-]?id)?|private[_-]?key|authorization|auth|token|secret|credentials?|signature|sig|password|passwd)(?:[_-][A-Za-z0-9_-]{1,64}|(?:value|data|token|secret|credential|password|header|field|id|key)[A-Za-z0-9_-]{0,48})?\b(?:\\?["'])?\s*[:=]\s*(?:\\?["'])?)(?!\[redacted\]|[A-Za-z0-9][A-Za-z0-9._~-]{0,63}(?:\s|%20|\+)+\[redacted\])(?:\\(?!["'])|[^\s"',;}&\\\]])+/giu, "$1[redacted]", ) .replaceAll(/sk-(?:proj-)?[A-Za-z0-9_*=-]{8,}/gu, "[redacted]") diff --git a/sdk/typescript/tests-ts/errors.test.ts b/sdk/typescript/tests-ts/errors.test.ts index 686b44a3..a48ec914 100644 --- a/sdk/typescript/tests-ts/errors.test.ts +++ b/sdk/typescript/tests-ts/errors.test.ts @@ -13,6 +13,11 @@ describe("security error redaction", () => { "provider failed: -----BEGIN RSA PRIVATE KEY-----\nSYNTHETIC_TRUNCATED_KEY", ), ).toBe("provider failed: [redacted]"); + expect( + redactedErrorMessage( + "provider failed: -----BEGIN PGP PRIVATE KEY BLOCK-----\nSYNTHETIC_PGP_PRIVATE_KEY\n-----END PGP PRIVATE KEY BLOCK----- safe=value", + ), + ).toBe("provider failed: [redacted] safe=value"); }); test("redacts every parameter from structured authorization schemes", () => { @@ -44,6 +49,11 @@ describe("security error redaction", () => { 'client_authorization_value=Digest username="example", response="SYNTHETIC_DIGEST_SECRET"', ), ).toBe("client_authorization_value=Digest [redacted]"); + expect( + redactedErrorMessage( + "Authorization: 0-custom 0nonce=visible, !response=SYNTHETIC_DIGEST_SECRET", + ), + ).toBe("Authorization: 0-custom [redacted]"); }); test("redacts encoded API-key names without consuming other parameters", () => { @@ -86,5 +96,8 @@ describe("security error redaction", () => { '{"api_keys_data":[{"key":"SYNTHETIC_ONE"},{"key":"SYNTHETIC_TWO"}]}', ), ).toBe('{"api_keys_data":"[redacted]"}'); + expect( + redactedErrorMessage("password=prefix\\SYNTHETIC_SECRET safe=visible"), + ).toBe("password=[redacted] safe=visible"); }); });