fix(anthropic-oauth): bound OAuth token endpoint response reads (#96644)

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 <noreply@anthropic.com>
(cherry picked from commit b1fae752f8)
This commit is contained in:
solodmd
2026-07-01 08:43:06 +08:00
committed by Dallin Romney
parent 2583a21d93
commit f3d4b174ad
2 changed files with 31 additions and 1 deletions
+22
View File
@@ -79,4 +79,26 @@ describe("Anthropic OAuth token responses", () => {
"Anthropic token refresh returned invalid token fields.",
);
});
it("rejects an oversized Anthropic token refresh response", async () => {
let pullCount = 0;
const cancel = vi.fn(async () => undefined);
const oversizedStream = new ReadableStream<Uint8Array>({
pull(controller) {
pullCount += 1;
controller.enqueue(new Uint8Array(pullCount === 1 ? 16 * 1024 * 1024 + 1 : 1));
},
cancel,
});
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response(oversizedStream, { status: 200 })),
);
await expect(refreshAnthropicToken("old-refresh-token")).rejects.toThrow("too large");
expect(pullCount).toBeLessThanOrEqual(2);
expect(cancel).toHaveBeenCalledOnce();
});
});
+9 -1
View File
@@ -6,6 +6,7 @@
*/
import type { Server } from "node:http";
import { readResponseWithLimit } from "@openclaw/media-core/read-response-with-limit";
import { toErrorObject } from "../../../infra/errors.js";
import {
generateOAuthState,
@@ -52,6 +53,10 @@ const CALLBACK_PATH = "/callback";
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
const SCOPES =
"org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
/** Max response body bytes for Anthropic OAuth token endpoint (16 MiB). */
const OAUTH_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
async function getNodeApis(): Promise<NodeApis> {
if (nodeApis) {
return nodeApis;
@@ -233,7 +238,10 @@ async function postJson(
signal: buildOAuthRequestSignal({ signal: options.signal, timeoutMs }),
});
const responseBody = await response.text();
const buffer = await readResponseWithLimit(response, OAUTH_RESPONSE_MAX_BYTES, {
onOverflow: ({ size }) => new Error(`Anthropic OAuth response too large: ${size} bytes`),
});
const responseBody = new TextDecoder().decode(buffer);
if (!response.ok) {
throw new Error(