From f3d4b174ad953dbce6afd2d17e71069a71d72445 Mon Sep 17 00:00:00 2001 From: solodmd Date: Wed, 1 Jul 2026 08:43:06 +0800 Subject: [PATCH] fix(anthropic-oauth): bound OAuth token endpoint response reads (#96644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (cherry picked from commit b1fae752f81eab4fcdc91629a408bc62cf2af0b2) --- src/llm/utils/oauth/anthropic.test.ts | 22 ++++++++++++++++++++++ src/llm/utils/oauth/anthropic.ts | 10 +++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/llm/utils/oauth/anthropic.test.ts b/src/llm/utils/oauth/anthropic.test.ts index 3033260b480b..4f9d15d93751 100644 --- a/src/llm/utils/oauth/anthropic.test.ts +++ b/src/llm/utils/oauth/anthropic.test.ts @@ -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({ + 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(); + }); }); diff --git a/src/llm/utils/oauth/anthropic.ts b/src/llm/utils/oauth/anthropic.ts index 5c98c9f8cc3a..3907c6da788a 100644 --- a/src/llm/utils/oauth/anthropic.ts +++ b/src/llm/utils/oauth/anthropic.ts @@ -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 { 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(