Skip to content

Commit 90be20d

Browse files
solodmdclaude
andcommitted
fix(anthropic-oauth): bound OAuth token endpoint response reads
postJson reads the Anthropic OAuth token endpoint response body with an unbounded await response.text(). A compromised or hijacked OAuth endpoint can stream an arbitrarily large body and force the runtime to buffer the entire payload before parsing — an OOM/DoS vector. Replace with readResponseWithLimit at 16 MiB cap + TextDecoder decode to match the sibling bounded-read pattern (provider-http-errors.ts:308). Co-Authored-By: Claude <[email protected]>
1 parent 643410c commit 90be20d

2 files changed

Lines changed: 31 additions & 1 deletion

File tree

src/llm/utils/oauth/anthropic.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,26 @@ describe("Anthropic OAuth token responses", () => {
7979
"Anthropic token refresh returned invalid token fields.",
8080
);
8181
});
82+
83+
it("rejects an oversized Anthropic token refresh response", async () => {
84+
let pullCount = 0;
85+
const cancel = vi.fn(async () => undefined);
86+
const oversizedStream = new ReadableStream<Uint8Array>({
87+
pull(controller) {
88+
pullCount += 1;
89+
controller.enqueue(new Uint8Array(pullCount === 1 ? 16 * 1024 * 1024 + 1 : 1));
90+
},
91+
cancel,
92+
});
93+
94+
vi.stubGlobal(
95+
"fetch",
96+
vi.fn(async () => new Response(oversizedStream, { status: 200 })),
97+
);
98+
99+
await expect(refreshAnthropicToken("old-refresh-token")).rejects.toThrow("too large");
100+
101+
expect(pullCount).toBeLessThanOrEqual(2);
102+
expect(cancel).toHaveBeenCalledOnce();
103+
});
82104
});

src/llm/utils/oauth/anthropic.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import type { Server } from "node:http";
9+
import { readResponseWithLimit } from "@openclaw/media-core/read-response-with-limit";
910
import { toErrorObject } from "../../../infra/errors.js";
1011
import {
1112
generateOAuthState,
@@ -52,6 +53,10 @@ const CALLBACK_PATH = "/callback";
5253
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
5354
const SCOPES =
5455
"org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
56+
57+
/** Max response body bytes for Anthropic OAuth token endpoint (16 MiB). */
58+
const OAUTH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
59+
5560
async function getNodeApis(): Promise<NodeApis> {
5661
if (nodeApis) {
5762
return nodeApis;
@@ -233,7 +238,10 @@ async function postJson(
233238
signal: buildOAuthRequestSignal({ signal: options.signal, timeoutMs }),
234239
});
235240

236-
const responseBody = await response.text();
241+
const buffer = await readResponseWithLimit(response, OAUTH_RESPONSE_MAX_BYTES, {
242+
onOverflow: ({ size }) => new Error(`Anthropic OAuth response too large: ${size} bytes`),
243+
});
244+
const responseBody = new TextDecoder().decode(buffer);
237245

238246
if (!response.ok) {
239247
throw new Error(

0 commit comments

Comments
 (0)