Skip to content

Commit 2f3bd16

Browse files
wangmiao0668000666RomneyDa
authored andcommitted
fix(oauth): bound github-copilot OAuth response reads at 16 MiB (openclaw#97499)
* fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update (cherry picked from commit bd0c052)
1 parent cfa366f commit 2f3bd16

2 files changed

Lines changed: 91 additions & 2 deletions

File tree

src/llm/utils/oauth/github-copilot.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,85 @@ describe("GitHub Copilot OAuth model policy", () => {
181181
expect(cancel).toHaveBeenCalledTimes(1);
182182
});
183183
});
184+
185+
describe("GitHub Copilot OAuth bounded reads", () => {
186+
it("caps oversized OAuth JSON responses instead of buffering the full body", async () => {
187+
// 18 MiB body in 1 MiB chunks exceeds the 16 MiB default cap on
188+
// the shared readProviderJsonResponse reader.
189+
const CHUNK = 1024 * 1024;
190+
const CHUNK_COUNT = 18;
191+
let pulls = 0;
192+
const encoder = new TextEncoder();
193+
const stream = new ReadableStream<Uint8Array>({
194+
pull(controller) {
195+
if (pulls >= CHUNK_COUNT) {
196+
controller.close();
197+
return;
198+
}
199+
pulls += 1;
200+
controller.enqueue(encoder.encode("a".repeat(CHUNK)));
201+
},
202+
});
203+
vi.stubGlobal(
204+
"fetch",
205+
vi.fn(
206+
async () =>
207+
new Response(stream, {
208+
status: 200,
209+
headers: { "Content-Type": "application/json" },
210+
}),
211+
),
212+
);
213+
214+
await expect(refreshGitHubCopilotToken("refresh-token")).rejects.toThrow(
215+
"GitHub Copilot token refresh request: JSON response exceeds 16777216 bytes",
216+
);
217+
});
218+
219+
it("parses normal-size OAuth JSON responses under the byte cap", async () => {
220+
vi.stubGlobal(
221+
"fetch",
222+
vi.fn(
223+
async () =>
224+
new Response(
225+
JSON.stringify({
226+
token: "copilot-token",
227+
expires_at: Math.floor(Date.now() / 1000) + 3600,
228+
}),
229+
{ status: 200, headers: { "Content-Type": "application/json" } },
230+
),
231+
),
232+
);
233+
234+
const result = await refreshGitHubCopilotToken("refresh-token");
235+
expect(result.access).toBe("copilot-token");
236+
expect(typeof result.expires).toBe("number");
237+
});
238+
239+
it("cancels the upstream body when the bounded reader overflows", async () => {
240+
const cancel = vi.fn(async () => undefined);
241+
const encoder = new TextEncoder();
242+
const source = new ReadableStream<Uint8Array>({
243+
pull(controller) {
244+
controller.enqueue(encoder.encode("a".repeat(1024 * 1024)));
245+
},
246+
cancel,
247+
});
248+
vi.stubGlobal(
249+
"fetch",
250+
vi.fn(
251+
async () =>
252+
new Response(source, {
253+
status: 200,
254+
headers: { "Content-Type": "application/json" },
255+
}),
256+
),
257+
);
258+
259+
await expect(refreshGitHubCopilotToken("refresh-token")).rejects.toThrow(
260+
"GitHub Copilot token refresh request",
261+
);
262+
263+
expect(cancel).toHaveBeenCalled();
264+
});
265+
});

src/llm/utils/oauth/github-copilot.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
*/
44

55
import { resolveTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
6+
import {
7+
readProviderJsonResponse,
8+
readProviderTextResponse,
9+
} from "../../../agents/provider-http-errors.js";
610
import {
711
nonNegativeSecondsToSafeMilliseconds,
812
positiveSecondsToSafeMilliseconds,
@@ -175,18 +179,21 @@ async function fetchResponse(
175179
}
176180
}
177181

182+
// Shared 16 MiB bounded reader — a hostile OAuth endpoint cannot force the
183+
// runtime to buffer an unbounded body through `.text()` / `.json()`.
178184
async function fetchJson(
179185
url: string,
180186
init: RequestInit,
181187
operation: string,
182188
options: CopilotRequestOptions = {},
183189
): Promise<unknown> {
184190
const response = await fetchResponse(url, init, operation, options);
191+
const label = `GitHub Copilot ${operation}`;
185192
if (!response.ok) {
186-
const text = await response.text();
193+
const text = await readProviderTextResponse(response, label);
187194
throw new Error(`${response.status} ${response.statusText}: ${text}`);
188195
}
189-
return response.json();
196+
return readProviderJsonResponse(response, label);
190197
}
191198

192199
async function startDeviceFlow(

0 commit comments

Comments
 (0)