From cff2bd8030c45610f79fba26cd4ec6e938df77a4 Mon Sep 17 00:00:00 2001 From: lin-hongkuan Date: Tue, 30 Jun 2026 01:51:08 +0800 Subject: [PATCH] fix(secrets): strip control characters from secret input (#96444) * fix(secrets): strip control characters from secret input * chore: retrigger PR checks * fix(web-content): strip controls from provider secrets --------- Co-authored-by: lin-hongkuan (cherry picked from commit bc7f0f1223cd760ab812eeab526e02ae6ae27d68) --- .../web-content-core/src/provider-runtime-shared.test.ts | 9 +++++++++ packages/web-content-core/src/provider-runtime-shared.ts | 7 ++++++- src/utils/normalize-secret-input.test.ts | 5 +++++ src/utils/normalize-secret-input.ts | 7 ++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/web-content-core/src/provider-runtime-shared.test.ts b/packages/web-content-core/src/provider-runtime-shared.test.ts index b10ddd08ccf6..c57d5a0aa7b0 100644 --- a/packages/web-content-core/src/provider-runtime-shared.test.ts +++ b/packages/web-content-core/src/provider-runtime-shared.test.ts @@ -30,6 +30,15 @@ describe("readWebProviderEnvValue", () => { it("normalizes env credentials before returning them", () => { expect(readWebProviderEnvValue(["API_KEY"], { API_KEY: " key\r\nvalue🙂 " })).toBe("keyvalue"); }); + + it("strips embedded controls from env credentials while preserving ordinary spaces", () => { + expect(readWebProviderEnvValue(["API_KEY"], { API_KEY: " sk-\u0000ab\tc\u007f\u0085 " })).toBe( + "sk-abc", + ); + expect(readWebProviderEnvValue(["API_KEY"], { API_KEY: " Bearer token value " })).toBe( + "Bearer token value", + ); + }); }); describe("hasWebProviderEntryCredential", () => { diff --git a/packages/web-content-core/src/provider-runtime-shared.ts b/packages/web-content-core/src/provider-runtime-shared.ts index 723d1d2d8a72..450821321ec2 100644 --- a/packages/web-content-core/src/provider-runtime-shared.ts +++ b/packages/web-content-core/src/provider-runtime-shared.ts @@ -56,7 +56,12 @@ function normalizeSecretInput(value: unknown): string { let latin1Only = ""; for (const char of collapsed) { const codePoint = char.codePointAt(0); - if (typeof codePoint === "number" && codePoint <= 0xff) { + const isControl = + typeof codePoint === "number" && + ((codePoint >= 0x00 && codePoint <= 0x1f) || + codePoint === 0x7f || + (codePoint >= 0x80 && codePoint <= 0x9f)); + if (typeof codePoint === "number" && codePoint <= 0xff && !isControl) { latin1Only += char; } } diff --git a/src/utils/normalize-secret-input.test.ts b/src/utils/normalize-secret-input.test.ts index 721f6d7ed98f..232c0a2954e6 100644 --- a/src/utils/normalize-secret-input.test.ts +++ b/src/utils/normalize-secret-input.test.ts @@ -14,6 +14,11 @@ describe("normalizeSecretInput", () => { expect(normalizeSecretInput(" sk-\r\nabc\n123 ")).toBe("sk-abc123"); }); + it("strips embedded control characters while preserving ordinary spaces", () => { + expect(normalizeSecretInput(" sk-\u0000ab\tc\u007f\u0085 ")).toBe("sk-abc"); + expect(normalizeSecretInput(" Bearer token value ")).toBe("Bearer token value"); + }); + it("drops non-Latin1 code points that can break HTTP ByteString headers", () => { // U+0417 (Cyrillic З) and U+2502 (box drawing │) are > 255. expect(normalizeSecretInput("key-\u0417\u2502-token")).toBe("key--token"); diff --git a/src/utils/normalize-secret-input.ts b/src/utils/normalize-secret-input.ts index 47b79b76b0df..ab5840f938dd 100644 --- a/src/utils/normalize-secret-input.ts +++ b/src/utils/normalize-secret-input.ts @@ -25,7 +25,12 @@ export function normalizeSecretInput(value: unknown): string { const chars: string[] = []; for (const char of collapsed) { const codePoint = char.codePointAt(0); - if (typeof codePoint === "number" && codePoint <= 0xff) { + const isControl = + typeof codePoint === "number" && + ((codePoint >= 0x00 && codePoint <= 0x1f) || + codePoint === 0x7f || + (codePoint >= 0x80 && codePoint <= 0x9f)); + if (typeof codePoint === "number" && codePoint <= 0xff && !isControl) { chars.push(char); } }