Skip to content

Commit 30dca40

Browse files
committed
fix(browser): decode CDP URL credentials
1 parent 587eefe commit 30dca40

3 files changed

Lines changed: 40 additions & 1 deletion

File tree

extensions/browser/src/browser/browser-utils.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ describe("cdp.helpers", () => {
199199
expect(headers.Authorization).toBe(`Basic ${Buffer.from("user:pass").toString("base64")}`);
200200
});
201201

202+
it("decodes percent-encoded basic auth credentials from URLs", () => {
203+
const headers = getHeadersWithAuth("https://alice:p%40ss%20word@example.com");
204+
expect(headers.Authorization).toBe(
205+
`Basic ${Buffer.from("alice:p@ss word").toString("base64")}`,
206+
);
207+
});
208+
202209
it("keeps preexisting authorization headers", () => {
203210
const headers = getHeadersWithAuth("https://user:pass@example.com", {
204211
Authorization: "Bearer token",

extensions/browser/src/browser/cdp.helpers.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ describe("cdp helpers", () => {
146146
expect(release).toHaveBeenCalledTimes(1);
147147
});
148148

149+
it("decodes URL credentials before sending guarded CDP auth headers", async () => {
150+
const release = vi.fn(async () => {});
151+
fetchWithSsrFGuardMock.mockResolvedValueOnce({
152+
response: {
153+
ok: true,
154+
status: 200,
155+
},
156+
release,
157+
});
158+
159+
await expect(
160+
fetchOk("http://alice:p%40ss%20word@127.0.0.1:9222/json/version", 250),
161+
).resolves.toBeUndefined();
162+
163+
const request = requireGuardedFetchRequest();
164+
expect(request?.url).toBe("http://127.0.0.1:9222/json/version");
165+
expect(request?.init?.headers).toEqual({
166+
Authorization: `Basic ${Buffer.from("alice:p@ss word").toString("base64")}`,
167+
});
168+
expect(release).toHaveBeenCalledTimes(1);
169+
});
170+
149171
it("preserves hostname allowlist while allowing exact loopback CDP fetches", async () => {
150172
const release = vi.fn(async () => {});
151173
fetchWithSsrFGuardMock.mockResolvedValueOnce({

extensions/browser/src/browser/cdp.helpers.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ export type CdpSendFn = (
113113
sessionId?: string,
114114
) => Promise<unknown>;
115115

116+
function decodeUrlUserInfo(value: string): string {
117+
try {
118+
return decodeURIComponent(value);
119+
} catch {
120+
return value;
121+
}
122+
}
123+
116124
function rawCdpMessageToString(data: WebSocket.RawData): string {
117125
if (typeof data === "string") {
118126
return data;
@@ -141,7 +149,9 @@ export function getHeadersWithAuth(url: string, headers: Record<string, string>
141149
return mergedHeaders;
142150
}
143151
if (parsed.username || parsed.password) {
144-
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
152+
const username = decodeUrlUserInfo(parsed.username);
153+
const password = decodeUrlUserInfo(parsed.password);
154+
const auth = Buffer.from(`${username}:${password}`).toString("base64");
145155
return { ...mergedHeaders, Authorization: `Basic ${auth}` };
146156
}
147157
} catch {

0 commit comments

Comments
 (0)