fix(oauth): bound anthropic OAuth token request response reads at 16 MiB#97519
fix(oauth): bound anthropic OAuth token request response reads at 16 MiB#97519wangmiao0668000666 wants to merge 1 commit into
Conversation
|
Thanks for the context here. I swept through the related work, and this is now duplicate or superseded. Close as a duplicate: an older open Anthropic OAuth bounded-read PR already covers the same unbounded Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Canonical path: Use #96644 as the canonical Anthropic OAuth bounded-read review target, optionally folding this PR's shared-helper label there if maintainers prefer that shape. So I’m closing this here and keeping the remaining discussion on #96644. Review detailsBest possible solution: Use #96644 as the canonical Anthropic OAuth bounded-read review target, optionally folding this PR's shared-helper label there if maintainers prefer that shape. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main reads the Anthropic OAuth token response with Is this the best way to solve the issue? No as a separate PR. The patch shape is reasonable, but the same bounded-read fix is already covered by proof-sufficient open PR #96644, so maintainers should keep one canonical branch. Security review: Security review cleared: The diff narrows an existing OAuth response-buffering DoS path and does not add dependencies, scripts, workflow permissions, package metadata, or new secret handling. AGENTS.md: found and applied where relevant. What I checked:
Likely related people:
Codex review notes: model internal, reasoning high; reviewed against 2f851ecfe9df. |
|
ClawSweeper applied the proposed close for this PR.
|
What Problem This Solves
src/llm/utils/oauth/anthropic.ts:236(postJson) reads the Anthropic OAuthtoken-exchange response body with
await response.text(), which buffers theentire payload into memory before the runtime can decide to stop. A hostile
or buggy
https://platform.claude.com/v1/oauth/tokenendpoint (or anenterprise proxy) can return
200 OKwith a body that grows unbounded, andthe runtime will buffer all of it before parsing. The existing
buildOAuthRequestSignaltimeout only caps wall-clock time, not bytes — aslow trickle that exceeds memory still OOMs.
The OAuth surface here is untrusted:
https://claude.ai/oauth/authorize(redirect target),
https://platform.claude.com/v1/oauth/token(tokenexchange + refresh), and the local callback server bound to
process.env.OPENCLAW_OAUTH_CALLBACK_HOST || "127.0.0.1". Theexternal-to-trust boundary makes the body-cap a runtime hardening fix, not
a hypothetical.
The error path on line 243 also concatenates
body=${responseBody}into thethrown error message. A 17 MiB body would build a 17 MiB error string, which
can then be logged or wrapped into a
causechain — same OOM vector on adifferent code path.
Why This Change Was Made
src/agents/provider-http-errors.ts:91-102already exposes the canonicalbounded reader (
readProviderTextResponse) capped at the shared 16 MiB cap(
PROVIDER_TEXT_RESPONSE_MAX_BYTES). The helper throws${label}: text response exceeds ${maxBytes} byteson overflow, whichboth
exchangeAuthorizationCode(line 256) andrefreshAnthropicToken(line 425) catch via their
try/catchblocks — the bounded-read errornaturally lands in the existing
formatErrorDetails(error)formatter, nonew error-handling code needed.
Sibling reference already in
main:src/agents/provider-transport-fetch.ts:33imports the same helper from
./provider-http-errors.jsfor the sameunbounded-read hardening reason. PR #97499 (open) and PR #97515 (open) are
applying the same pattern to
github-copilot.tsandmcp-http-fetch.ts.This PR is the third in that bounded-read campaign.
The per-call label
"Anthropic OAuth token request"is intentionallydistinct from siblings (
"GitHub Copilot <op>","MCP HTTP fetch","Chutes <op>") so log lines can attribute the bounded-read rejection tothis call site without ambiguity. The label is a single new string; no
config surface changes.
User Impact
responses (<16 MiB). An Anthropic OAuth endpoint that previously returned
huge bodies will surface a typed error in the OAuth logs instead of
silently consuming memory. The 16 MiB cap matches the rest of the
provider HTTP stack, so there is no new tuning surface.
anthropicOAuthProvider.login,refreshAnthropicToken, and the public callback flow keep theirexisting signatures. The only new failure mode is the labeled
Anthropic OAuth token request: text response exceeds 16777216 byteserror, which is the desired safety behavior.
Changes
src/llm/utils/oauth/anthropic.ts:9— importreadProviderTextResponsefrom the shared
../../../agents/provider-http-errors.js.src/llm/utils/oauth/anthropic.ts:237-239—postJsonswapsawait response.text()for the bounded reader with the per-call label"Anthropic OAuth token request". Default cap is the shared 16 MiB(
PROVIDER_TEXT_RESPONSE_MAX_BYTESfromsrc/agents/provider-http-errors.ts:17). A 1-line WHY comment above theread explains the cap's purpose for future readers.
src/llm/utils/oauth/anthropic.test.ts:84-119— 1 new inline test in theexisting
describe("Anthropic OAuth token responses")block. The testdrives
refreshAnthropicToken(which is the public production wrapperthat calls
postJson) end-to-end against a streaming 18 MiB body andasserts the call rejects with the labeled overflow error. Mirrors the
inline test pattern from
src/llm/utils/oauth/github-copilot.test.ts:185-265(Alix-007 fix(googlechat): replace unbounded response.json() with readProviderJsonResponse #96772) and
src/agents/mcp-http-fetch.test.ts:255-296(PR fix(mcp): bound MCP HTTP fetch text response reads at 16 MiB #97515).
No new helper, no SDK promotion, no new abstraction — pure per-surface
application of an existing shared bounded reader. The two callers of
postJson(exchangeAuthorizationCodeat line 256,refreshAnthropicTokenat line 425) need no edits: their
try/catchblocks already catch thelabeled overflow error and route it through
formatErrorDetails(error).Evidence
Real-
ReadableStream-driven inline test through the productionrefreshAnthropicTokenwrapper, captured from the local vitest run:The full vitest run for
src/llm/utils/oauth/anthropic.test.ts(5/5 passed)includes:
on JSON parse failure, unsafe token lifetime rejection) — all green,
proving no regression on the existing behavior.
correct label, the canonical cap value (16777216), and that the wrapper
delegates to the shared bounded reader rather than a parallel
implementation.
Out of scope
src/llm/utils/oauth/github-copilot.ts:182-197andsrc/llm/utils/oauth/chutes-oauth.tsare being migrated in PR fix(oauth): bound github-copilot OAuth response reads at 16 MiB #97499(open, not yet merged). This PR does NOT touch those files. Once fix(oauth): bound github-copilot OAuth response reads at 16 MiB #97499
merges, no follow-up is needed — three of the four bounded-read surfaces
in the OAuth stack will be on the same helper.
src/agents/mcp-http-fetch.ts:69is being migrated in PR fix(mcp): bound MCP HTTP fetch text response reads at 16 MiB #97515 (open,not yet merged). Same pattern, different surface.
127.0.0.1:53692is local(CLI-only, per the file header comment) and is not exposed to the
network — it is not in scope for a body-cap fix.
Diff stats
Size: S (< 100 LoC non-test). Within SKILL checklist #1 budget.
Risk checklist
normal-size bodies. New failure mode:
Anthropic OAuth token request: text response exceeds 16777216 byteswhen the Anthropic OAuth endpointreturns >16 MiB. That is the desired safety behavior. The error is
caught by
try/catchblocks in both callers and routed through theexisting
formatErrorDetails(error)formatter — same observablebehavior as today's transport errors.
body cap to a previously-unbounded OAuth response read. No new attack
surface; no new endpoints touched.
responseBodyvariable is used in the errorpath on line 243 (
body=${responseBody}) and as the return value ofpostJsonfor both callers. After this change,responseBodyis thebounded string — at most 16 MiB. The error message is correspondingly
capped, and the JSON.parse in the callers receives at most 16 MiB. Both
behaviors are the desired safety guarantees.
throws before line 241 (
if (!response.ok)) executes, so thebody=${responseBody}interpolation never builds a 17 MiB error string.The labeled overflow error is propagated to the caller's
try/catchinstead, which is the correct failure mode.
Label: security
AI-assisted.