fix(secrets): harden api key normalization for ByteString headers

This commit is contained in:
Vignesh Natarajan
2026-03-05 18:31:35 -08:00
parent 7a22b3fa0b
commit 1ab9393212
4 changed files with 78 additions and 1 deletions

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { normalizeOptionalSecretInput, normalizeSecretInput } from "./normalize-secret-input.js";
describe("normalizeSecretInput", () => {
it("returns empty string for non-string values", () => {
expect(normalizeSecretInput(undefined)).toBe("");
expect(normalizeSecretInput(null)).toBe("");
expect(normalizeSecretInput(123)).toBe("");
expect(normalizeSecretInput({})).toBe("");
});
it("strips embedded line breaks and surrounding whitespace", () => {
expect(normalizeSecretInput(" sk-\r\nabc\n123 ")).toBe("sk-abc123");
});
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");
});
it("preserves Latin-1 characters and internal spaces", () => {
expect(normalizeSecretInput(" café token ")).toBe("café token");
});
});
describe("normalizeOptionalSecretInput", () => {
it("returns undefined when normalized value is empty", () => {
expect(normalizeOptionalSecretInput(" \r\n ")).toBeUndefined();
expect(normalizeOptionalSecretInput("\u0417\u2502")).toBeUndefined();
});
it("returns normalized value when non-empty", () => {
expect(normalizeOptionalSecretInput(" key-\u0417 ")).toBe("key-");
});
});

View File

@@ -4,6 +4,12 @@
* Common footgun: line breaks (especially `\r`) embedded in API keys/tokens.
* We strip line breaks anywhere, then trim whitespace at the ends.
*
* Another frequent source of runtime failures is rich-text/Unicode artifacts
* (smart punctuation, box-drawing chars, etc.) pasted into API keys. These can
* break HTTP header construction (`ByteString` violations). Drop non-Latin1
* code points so malformed keys fail as auth errors instead of crashing request
* setup.
*
* Intentionally does NOT remove ordinary spaces inside the string to avoid
* silently altering "Bearer <token>" style values.
*/
@@ -11,7 +17,15 @@ export function normalizeSecretInput(value: unknown): string {
if (typeof value !== "string") {
return "";
}
return value.replace(/[\r\n\u2028\u2029]+/g, "").trim();
const collapsed = value.replace(/[\r\n\u2028\u2029]+/g, "");
let latin1Only = "";
for (const char of collapsed) {
const codePoint = char.codePointAt(0);
if (typeof codePoint === "number" && codePoint <= 0xff) {
latin1Only += char;
}
}
return latin1Only.trim();
}
export function normalizeOptionalSecretInput(value: unknown): string | undefined {