fix(oauth): harden browser launch handling

This commit is contained in:
Armin Ronacher
2026-06-02 23:15:46 +02:00
parent a98e087e5d
commit ba6e5298df
6 changed files with 138 additions and 17 deletions

View File

@@ -84,6 +84,97 @@ describe("GitHub Copilot OAuth device flow", () => {
await loginPromise;
});
it("rejects a non-http(s) verification_uri before it reaches onDeviceCode", async () => {
// A malicious enterprise OAuth server could return a verification_uri that
// the browser launcher would otherwise hand to the OS. Ensure such values
// are rejected at the deserialization boundary.
const fetchMock = vi.fn(async (input: unknown): Promise<Response> => {
const url = getUrl(input);
if (url.endsWith("/login/device/code")) {
return jsonResponse({
device_code: "device-code",
user_code: "ABCD-EFGH",
verification_uri: "$(id>/tmp/pwned)",
interval: 1,
expires_in: 900,
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const onDeviceCode = vi.fn();
await expect(
loginGitHubCopilot({
onDeviceCode,
onPrompt: async () => "",
}),
).rejects.toThrow(/Untrusted verification_uri/);
expect(onDeviceCode).not.toHaveBeenCalled();
});
it("normalizes verification_uri before it reaches onDeviceCode", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-03-09T00:00:00Z"));
const rawVerificationUri = "https://github.com/login/\x1b]8;;evil";
const normalizedVerificationUri = new URL(rawVerificationUri).href;
expect(normalizedVerificationUri).not.toBe(rawVerificationUri);
const fetchMock = vi.fn(async (input: unknown): Promise<Response> => {
const url = getUrl(input);
if (url.endsWith("/login/device/code")) {
return jsonResponse({
device_code: "device-code",
user_code: "ABCD-EFGH",
verification_uri: rawVerificationUri,
interval: 1,
expires_in: 900,
});
}
if (url.endsWith("/login/oauth/access_token")) {
return jsonResponse({ access_token: "ghu_refresh_token" });
}
if (url.includes("/copilot_internal/v2/token")) {
return jsonResponse({
token: "tid=test;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;",
expires_at: 9999999999,
});
}
if (url.includes("/models/") && url.endsWith("/policy")) {
return new Response("", { status: 200 });
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const onDeviceCode = vi.fn();
const loginPromise = loginGitHubCopilot({
onDeviceCode,
onPrompt: async () => "",
});
await vi.advanceTimersByTimeAsync(0);
expect(onDeviceCode).toHaveBeenCalledWith({
userCode: "ABCD-EFGH",
verificationUri: normalizedVerificationUri,
intervalSeconds: 1,
expiresInSeconds: 900,
});
expect(onDeviceCode).not.toHaveBeenCalledWith(expect.objectContaining({ verificationUri: rawVerificationUri }));
await vi.advanceTimersByTimeAsync(1000);
await loginPromise;
});
it("polls immediately and increases the interval after slow_down", async () => {
vi.useFakeTimers();
const startTime = new Date("2026-03-09T00:00:00Z");