fix(google): bound google OAuth fetchWithTimeout arrayBuffer at 16 MiB#97628
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 29, 2026, 12:46 AM ET / 04:46 UTC. Summary PR surface: Source +20, Tests +172. Total +192 across 2 files. Reproducibility: yes. Source inspection of current main shows Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the helper-boundary cap if maintainers accept the oversized-response failure mode, while keeping the parser-side hardening in the related Google OAuth PR on its own track. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection of current main shows Is this the best way to solve the issue? Yes, with maintainer acceptance of the failure mode. The shared fetch helper is the right owner boundary for the network-read cap, while the related parser-cap PR remains a separate defense-in-depth layer. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against bf66b4e1ea7e. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +20, Tests +172. Total +192 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper re-review Per your P1 finding in #97628 (🦪, 2026-06-29 02:22Z), added Layer 3 real-wire Changes since first review
Local proof
Why bypassing |
|
🦞🧹 I asked ClawSweeper to review this item again. |
… reads ClawSweeper rated the prior proof 🦪 (mocked-only). Add two new tests that exercise readResponseWithLimit against a real http.createServer listener on 127.0.0.1:0 + real fetch(): one streams an 18 MiB chunked response and asserts the reported size satisfies MAX < got < TOTAL (cap fired AND no buffering beyond the server's full 18 MiB); the other returns a 45-byte OAuth JSON and asserts the body comes back exactly. Bypasses fetchWithSsrFGuard by design (production guard blocks loopback); Layer 1 mocked tests still cover the fetchWithTimeout orchestration end-to-end. Together the four tests cover both ends of the call chain.
e283731 to
936455b
Compare
What Problem This Solves
extensions/google/oauth.http.ts:21is the shared HTTP helper that wrapsfetchWithSsrFGuardfor every Google OAuth request. Two callers depend on it:extensions/google/oauth.project.ts— userinfo / pollOperation / loadCodeAssist / discoverProject (4 rawresponse.json()calls).extensions/google/oauth.token.ts—requestTokenGrant(1 rawresponse.text()+ 1 rawresponse.json()).The shared helper at line 21 reads the response body via
await response.arrayBuffer()— unbounded, swallows the entire body into aResponsebefore the caller sees it. A hostile or broken Google OAuth endpoint (or anyaccounts.google.commirror / enterprise proxy) can return a multi-gigabyte body and force OpenClaw to buffer all of it before any caller-side cap (including hugenshen's #97587readProviderJsonResponsecap) gets a chance to fire.This is the shared entry-point of every Google OAuth request. Capping here is the highest-leverage place to bound the surface — one change protects every caller (current and future), including the per-call-site caps being added by #97587.
Why This Change Was Made
readResponseWithLimitis the established bounded-read helper across the extension surface:openclaw/plugin-sdk/response-limit-runtimefor plugin consumption.mainfor the same defensive cap on their own fetch helpers:extensions/azure-speech/tts.ts:9,extensions/google/video-generation-provider.ts,extensions/openai/video-generation-provider.ts,extensions/xai/video-generation-provider.ts,extensions/byteplus/video-generation-provider.ts,extensions/fal/video-generation-provider.ts,extensions/minimax/video-generation-provider.ts,extensions/together/video-generation-provider.ts,extensions/openrouter/video-generation-provider.ts,extensions/runway/video-generation-provider.ts,extensions/comfy/workflow-runtime.ts, and more.extensions/googlechat/src/google-auth.runtime.ts:454(with the samefetchWithSsrFGuard+Buffer→Responsewrap-shape).Capping at 16 MiB matches the shared
PROVIDER_TEXT_RESPONSE_MAX_BYTES = 16 * 1024 * 1024cap fromsrc/agents/provider-http-errors.ts:17and thereadProviderJsonResponse/readProviderTextResponsedefaults used by #97587 — all three layers (this PR + #97587 + existing per-call-site caps) use the same 16 MiB cap.new Response(bufferBytes, ...)wraps aBuffer(NodeUint8Arrayview) into a globalResponse, preserving the existingPromise<Response>contract thatoauth.project.tsandoauth.token.tsalready consume.User Impact
google HTTP fetch: body exceeds 16777216 bytes (got <size>)when an OAuth endpoint streams >16 MiB, which is the desired safety behavior.fetchWithTimeoutcontract (Promise<Response>) is unchanged — no caller-side migration needed. This PR is non-breaking for all existing callers.Changes
extensions/google/oauth.http.ts:6— addimport { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";extensions/google/oauth.http.ts:9— addconst GOOGLE_OAUTH_BODY_MAX_BYTES = 16 * 1024 * 1024;extensions/google/oauth.http.ts:24-43— replaceawait response.arrayBuffer()withawait readResponseWithLimit(response, GOOGLE_OAUTH_BODY_MAX_BYTES, { onOverflow: ... }); wrap the returnedBufferin aUint8Arrayview for thenew Response(...)constructor.extensions/google/oauth.http.test.ts— new file, 2 inline tests through the productionfetchWithTimeoutwrapper:caps oversized response body at 16 MiB with labeled overflow error— drivesfetchWithTimeoutend-to-end against avi.mock-stubbedfetchWithSsrFGuardthat returns an 18 MiB body in 1 MiB chunks; asserts the call throwsgoogle HTTP fetch: body exceeds 16777216 bytes (got 17825792).returns a Response for normal-size bodies— drivesfetchWithTimeoutagainst a stubbedfetchWithSsrFGuardreturning a typical OAuth token JSON; asserts the wrappedResponseparses correctly through.json().Both tests use the same
vi.mockstyle as the existingextensions/google/oauth.http.proxy.test.ts(which covers env-proxy mode selection on 4 cases), keeping the test surface small and avoiding loopbackhttp.createServercomplexity.release()is asserted astoHaveBeenCalledOnce()in both tests to confirm thetry/finallycleanup runs on both happy and overflow paths.No new helper, no SDK promotion, no new abstraction, no
package.jsonchange, nodocs/**change. No edits toextensions/google/oauth.project.tsorextensions/google/oauth.token.ts— those are hugenshen's #97587 territory.Evidence
This section is a structured, verifiable proof that the change works. All commands and outputs are reproducible from a clean clone of
fix/google-http-bound-array-buffer-16mibat the head SHA below.Two-layer proof (per
real-behavior-proof-structure.md)Layer 1: Production
readResponseWithLimitcap fires throughfetchWithTimeout(negative control)Layer 2: Normal-size OAuth response passes through the bounded reader (happy path)
Combined evidence
pnpm test extensions/google— 363/363 tests pass across 28 files (including the 2 new tests + 4 existingoauth.http.proxy.test.tstests).pnpm tsgo:extensions— exit 0 (no type errors).node scripts/run-oxlint.mjs --tsconfig config/tsconfig/oxlint.extensions.json extensions/google/oauth.http.ts extensions/google/oauth.http.test.ts— exit 0 (no lint errors).git diff --numstat—extensions/google/oauth.http.ts +26/-2(S < 100 LoC non-test prod).new file—extensions/google/oauth.http.test.ts(2 inline tests, ~55 LoC).Reproduce locally:
What was not tested:
oauth2.googleapis.comwould never stream >16 MiB; the bounded-read contract is exactly about defending against adversarial/buggy servers, which the mockedReadableStreamsimulates faithfully).http.createServerproof (rejected for this PR — the existing test infrastructure already usesvi.mockforfetchWithSsrFGuard, and the inlineReadableStreamtest exercises the samereadResponseWithLimitcode path the production wire would. If reviewers ask for a loopback proof, it can be added as a follow-up.)Related
fix(google): bound OAuth project and token JSON response reads. Modifiesextensions/google/oauth.project.tsandextensions/google/oauth.token.tsto usereadProviderJsonResponse/readProviderTextResponse(16 MiB cap) at the call sites. Non-overlapping sub-surface: fix(google): bound OAuth project and token JSON response reads #97587 caps at the call sites, this PR caps at the shared entry point. Both PRs landing together gives defense in depth; either PR can land independently without blocking the other. The PR body in this PR cross-references fix(google): bound OAuth project and token JSON response reads #97587 in the## Why This Change Was Madesection.extensions/azure-speech/tts.ts:9— direct precedent forreadResponseWithLimitimport fromopenclaw/plugin-sdk/response-limit-runtimein a plugin helper.extensions/googlechat/src/google-auth.runtime.ts:454— same package, same wrap-shape (fetchWithSsrFGuard+Buffer→Response), confirms the cast pattern is sound.src/agents/provider-http-errors.ts:16-17—PROVIDER_TEXT_RESPONSE_MAX_BYTES = 16 * 1024 * 1024andPROVIDER_JSON_RESPONSE_MAX_BYTES = 16 * 1024 * 1024— the shared 16 MiB cap constant used byreadProviderJsonResponse/readProviderTextResponseand matched byGOOGLE_OAUTH_BODY_MAX_BYTESin this PR.Label: security
AI-assisted.