Skip to content

fix(oauth): bound anthropic OAuth token request response reads at 16 MiB#97519

Closed
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/anthropic-oauth-bound
Closed

fix(oauth): bound anthropic OAuth token request response reads at 16 MiB#97519
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/anthropic-oauth-bound

Conversation

@wangmiao0668000666

Copy link
Copy Markdown
Contributor

What Problem This Solves

src/llm/utils/oauth/anthropic.ts:236 (postJson) reads the Anthropic OAuth
token-exchange response body with await response.text(), which buffers the
entire payload into memory before the runtime can decide to stop. A hostile
or buggy https://platform.claude.com/v1/oauth/token endpoint (or an
enterprise proxy) can return 200 OK with a body that grows unbounded, and
the runtime will buffer all of it before parsing. The existing
buildOAuthRequestSignal timeout only caps wall-clock time, not bytes — a
slow 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 (token
exchange + refresh), and the local callback server bound to
process.env.OPENCLAW_OAUTH_CALLBACK_HOST || "127.0.0.1". The
external-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 the
thrown error message. A 17 MiB body would build a 17 MiB error string, which
can then be logged or wrapped into a cause chain — same OOM vector on a
different code path.

Why This Change Was Made

src/agents/provider-http-errors.ts:91-102 already exposes the canonical
bounded reader (readProviderTextResponse) capped at the shared 16 MiB cap
(PROVIDER_TEXT_RESPONSE_MAX_BYTES). The helper throws
${label}: text response exceeds ${maxBytes} bytes on overflow, which
both exchangeAuthorizationCode (line 256) and refreshAnthropicToken
(line 425) catch via their try/catch blocks — the bounded-read error
naturally lands in the existing formatErrorDetails(error) formatter, no
new error-handling code needed.

Sibling reference already in main: src/agents/provider-transport-fetch.ts:33
imports the same helper from ./provider-http-errors.js for the same
unbounded-read hardening reason. PR #97499 (open) and PR #97515 (open) are
applying the same pattern to github-copilot.ts and mcp-http-fetch.ts.
This PR is the third in that bounded-read campaign.

The per-call label "Anthropic OAuth token request" is intentionally
distinct from siblings ("GitHub Copilot <op>", "MCP HTTP fetch",
"Chutes <op>") so log lines can attribute the bounded-read rejection to
this call site without ambiguity. The label is a single new string; no
config surface changes.

User Impact

  • End users: No behavior change for normal-size Anthropic OAuth
    responses (<16 MiB). An Anthropic OAuth endpoint that previously returned

    16 MiB would silently buffer forever (or OOM the host); now it returns
    a clean, logged error within milliseconds.

  • Operators behind enterprise proxies: Misbehaving proxies that emit
    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.
  • No API surface change: anthropicOAuthProvider.login,
    refreshAnthropicToken, and the public callback flow keep their
    existing signatures. The only new failure mode is the labeled
    Anthropic OAuth token request: text response exceeds 16777216 bytes
    error, which is the desired safety behavior.

Changes

  • src/llm/utils/oauth/anthropic.ts:9 — import readProviderTextResponse
    from the shared ../../../agents/provider-http-errors.js.
  • src/llm/utils/oauth/anthropic.ts:237-239postJson swaps
    await 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_BYTES from
    src/agents/provider-http-errors.ts:17). A 1-line WHY comment above the
    read explains the cap's purpose for future readers.
  • src/llm/utils/oauth/anthropic.test.ts:84-119 — 1 new inline test in the
    existing describe("Anthropic OAuth token responses") block. The test
    drives refreshAnthropicToken (which is the public production wrapper
    that calls postJson) end-to-end against a streaming 18 MiB body and
    asserts 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 (exchangeAuthorizationCode at line 256, refreshAnthropicToken
at line 425) need no edits: their try/catch blocks already catch the
labeled overflow error and route it through formatErrorDetails(error).

Evidence

Real-ReadableStream-driven inline test through the production
refreshAnthropicToken wrapper, captured from the local vitest run:

PASS  |unit| src/llm/utils/oauth/anthropic.test.ts > Anthropic OAuth token responses
       > caps oversized Anthropic OAuth responses at 16 MiB instead of buffering the full body
       duration: 118ms

Test asserts:
  - body length:        18874368 bytes (18 MiB in 18 chunks of 1 MiB each)
  - cap (PROVIDER_TEXT_RESPONSE_MAX_BYTES): 16777216 bytes (16 MiB)
  - throw message:      "Anthropic OAuth token request: text response exceeds 16777216 bytes"
  - 18 MiB - 16 MiB = 2 MiB (rejected by the cap)
  - existing 4 tests still pass (cancel, JSON parse error, unsafe lifetime, etc.)

The full vitest run for src/llm/utils/oauth/anthropic.test.ts (5/5 passed)
includes:

  • 4 existing tests (cancel before flow, cancel during setup, no token echo
    on JSON parse failure, unsafe token lifetime rejection) — all green,
    proving no regression on the existing behavior.
  • 1 new test (oversized body triggers cap) — proves the cap fires with the
    correct label, the canonical cap value (16777216), and that the wrapper
    delegates to the shared bounded reader rather than a parallel
    implementation.
Test Files  1 passed (1)
     Tests  5 passed (5)
  Start at  23:49:15
  Duration  1.56s

Out of scope

Diff stats

2 files changed, 39 insertions(+), 1 deletion(-)
  src/llm/utils/oauth/anthropic.ts         +4/-1   (1 import + 1 comment + 2 line changes in postJson)
  src/llm/utils/oauth/anthropic.test.ts    +35/-0  (1 new inline test in existing describe block)

Size: S (< 100 LoC non-test). Within SKILL checklist #1 budget.

Risk checklist

  • User-visible behavior change? Minimal — same shape, same errors for
    normal-size bodies. New failure mode: Anthropic OAuth token request: text response exceeds 16777216 bytes when the Anthropic OAuth endpoint
    returns >16 MiB. That is the desired safety behavior. The error is
    caught by try/catch blocks in both callers and routed through the
    existing formatErrorDetails(error) formatter — same observable
    behavior as today's transport errors.
  • Config/env/migration change? No.
  • Security/auth/secrets/network change? Yes, positively — adds 16 MiB
    body cap to a previously-unbounded OAuth response read. No new attack
    surface; no new endpoints touched.
  • Highest-risk area: the responseBody variable is used in the error
    path on line 243 (body=${responseBody}) and as the return value of
    postJson for both callers. After this change, responseBody is the
    bounded 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.
  • Error path on line 243: when the body is >16 MiB, the bounded reader
    throws before line 241 (if (!response.ok)) executes, so the
    body=${responseBody} interpolation never builds a 17 MiB error string.
    The labeled overflow error is propagated to the caller's try/catch
    instead, which is the correct failure mode.

Label: security

AI-assisted.

@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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 postJson response-read problem, has positive ClawSweeper proof, and is the safer canonical review target.

Root-cause cluster
Relationship: duplicate
Canonical: #96644
Summary: The same Anthropic OAuth postJson unbounded token-response read is already covered by an older open proof-positive PR; other bounded-read campaign PRs are adjacent surfaces rather than the same root cause.

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 details

Best 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 response.text() before status handling; I did not run tests because this review was read-only.

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:

  • llagy009: git blame attributes the current Anthropic OAuth helper, its postJson raw response read, and the shared bounded-reader helpers in this checkout to commit 6c5a9fd. (role: introduced behavior; confidence: medium; commits: 6c5a9fde9f1c; files: src/llm/utils/oauth/anthropic.ts, src/agents/provider-http-errors.ts, packages/media-core/src/read-response-with-limit.ts)
  • vincentkoc: Live PR metadata for fix(msteams): truncate reflection prompt on a UTF-16 boundary #96578 shows this user merged the commit that introduced the current Anthropic OAuth helper in this checkout. (role: merger; confidence: medium; commits: 6c5a9fde9f1c; files: src/llm/utils/oauth/anthropic.ts)
  • solodmd: Authored the older open proof-positive PR that already addresses the same Anthropic OAuth bounded-read problem. (role: canonical fix contributor; confidence: medium; commits: 90be20d5f369; files: src/llm/utils/oauth/anthropic.ts, src/llm/utils/oauth/anthropic.test.ts)

Codex review notes: model internal, reasoning high; reviewed against 2f851ecfe9df.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 28, 2026
@clawsweeper

clawsweeper Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

  • Action: closed this PR.
  • Close reason: duplicate or superseded.
  • Evidence: durable ClawSweeper review.
  • Coverage proof: PR B clearly covers PR A's core behavior and review target: the same Anthropic OAuth postJson unbounded token-response read, with the same 16 MiB bounded-read hardening intent. Any PR A-specific helper wording or duplicate-report details are incidental and can be handled on PR B. Covering PR: fix(anthropic-oauth): bound OAuth token endpoint response reads #96644.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XS status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant