From 1cf6ff3bdc08a6ac08facb1006b1d7aabc0eaff4 Mon Sep 17 00:00:00 2001 From: VectorPeak Date: Wed, 1 Jul 2026 19:14:20 +0800 Subject: [PATCH] fix(browser): decode CDP URL credentials --- .../browser/src/browser/browser-utils.test.ts | 7 ++++++ .../browser/src/browser/cdp.helpers.test.ts | 22 +++++++++++++++++++ extensions/browser/src/browser/cdp.helpers.ts | 12 +++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/extensions/browser/src/browser/browser-utils.test.ts b/extensions/browser/src/browser/browser-utils.test.ts index 99c1601f1224..a98a9fffabad 100644 --- a/extensions/browser/src/browser/browser-utils.test.ts +++ b/extensions/browser/src/browser/browser-utils.test.ts @@ -199,6 +199,13 @@ describe("cdp.helpers", () => { expect(headers.Authorization).toBe(`Basic ${Buffer.from("user:pass").toString("base64")}`); }); + it("decodes percent-encoded basic auth credentials from URLs", () => { + const headers = getHeadersWithAuth("https://alice:p%40ss%20word@example.com"); + expect(headers.Authorization).toBe( + `Basic ${Buffer.from("alice:p@ss word").toString("base64")}`, + ); + }); + it("keeps preexisting authorization headers", () => { const headers = getHeadersWithAuth("https://user:pass@example.com", { Authorization: "Bearer token", diff --git a/extensions/browser/src/browser/cdp.helpers.test.ts b/extensions/browser/src/browser/cdp.helpers.test.ts index c7b851cc0bdc..1699fb1defd5 100644 --- a/extensions/browser/src/browser/cdp.helpers.test.ts +++ b/extensions/browser/src/browser/cdp.helpers.test.ts @@ -146,6 +146,28 @@ describe("cdp helpers", () => { expect(release).toHaveBeenCalledTimes(1); }); + it("decodes URL credentials before sending guarded CDP auth headers", async () => { + const release = vi.fn(async () => {}); + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: { + ok: true, + status: 200, + }, + release, + }); + + await expect( + fetchOk("http://alice:p%40ss%20word@127.0.0.1:9222/json/version", 250), + ).resolves.toBeUndefined(); + + const request = requireGuardedFetchRequest(); + expect(request?.url).toBe("http://127.0.0.1:9222/json/version"); + expect(request?.init?.headers).toEqual({ + Authorization: `Basic ${Buffer.from("alice:p@ss word").toString("base64")}`, + }); + expect(release).toHaveBeenCalledTimes(1); + }); + it("preserves hostname allowlist while allowing exact loopback CDP fetches", async () => { const release = vi.fn(async () => {}); fetchWithSsrFGuardMock.mockResolvedValueOnce({ diff --git a/extensions/browser/src/browser/cdp.helpers.ts b/extensions/browser/src/browser/cdp.helpers.ts index 1700ce81001c..5e02a3882fd6 100644 --- a/extensions/browser/src/browser/cdp.helpers.ts +++ b/extensions/browser/src/browser/cdp.helpers.ts @@ -113,6 +113,14 @@ export type CdpSendFn = ( sessionId?: string, ) => Promise; +function decodeUrlUserInfo(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return value; + } +} + function rawCdpMessageToString(data: WebSocket.RawData): string { if (typeof data === "string") { return data; @@ -141,7 +149,9 @@ export function getHeadersWithAuth(url: string, headers: Record return mergedHeaders; } if (parsed.username || parsed.password) { - const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64"); + const username = decodeUrlUserInfo(parsed.username); + const password = decodeUrlUserInfo(parsed.password); + const auth = Buffer.from(`${username}:${password}`).toString("base64"); return { ...mergedHeaders, Authorization: `Basic ${auth}` }; } } catch {