fix(exa): bound untrusted search JSON response reads#96038
Conversation
Exa search success responses were read via an unbounded `await response.json()`, so a misbehaving or hostile endpoint could stream an arbitrarily large body into memory before parsing. Read the success body through the shared bounded reader (16 MiB cap, the same limit other bundled providers use) and cancel the stream on overflow. This mirrors the error-body bound already in place and the openclaw#95103/openclaw#95108 response -limit campaign on the success-JSON side. AI-assisted.
|
@clawsweeper review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Codex review: needs maintainer review before merge. Reviewed June 23, 2026, 5:05 PM ET / 21:05 UTC. Summary PR surface: Source +13, Tests +48. Total +61 across 2 files. Reproducibility: yes. Current main's Exa success path calls response.json() in readExaSearchResults, and the PR body gives a concrete node:http streaming-server proof plus negative control for the oversized-body behavior. Review metrics: 1 noteworthy metric.
Stored data model 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 bounded Exa read with its regression coverage once maintainers accept the 16 MiB response cap; track any broader web-search response-limit sweep separately. Do we have a high-confidence way to reproduce the issue? Yes. Current main's Exa success path calls response.json() in readExaSearchResults, and the PR body gives a concrete node:http streaming-server proof plus negative control for the oversized-body behavior. Is this the best way to solve the issue? Yes. Reusing the exported plugin SDK bounded reader in the provider-owned Exa parser is the narrowest maintainable fix; a broader sibling sweep can happen separately without blocking this focused hardening. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 654544b6b7c4. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +13, Tests +48. Total +61 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
|
|
Merge-ready from my review. This follows the same response-body cap pattern as #96042 and the sibling Exa/Parallel/Ollama PRs: replace one unbounded success JSON read from an untrusted provider endpoint with the existing bounded reader, preserve normal small-response behavior, and add focused overflow/cancel coverage. I ran local autoreview in the PR worktree against Best-fix verdict: this is the right narrow fix because it reuses the existing bounded response-body helper instead of adding a broader provider refactor. |
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
The batch status read (fetchVoyageBatchStatus) parsed its response with an
unbounded await res.json(), and the batch error-file read (readVoyageBatchError)
buffered the whole body via await res.text(). On top of that, the non-OK
(4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did
await res.text() before throwing, and the non-OK output-file branch in
runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and
reachable via SSRF, so a misbehaving or hostile endpoint could stream an
unbounded body into memory on any of these paths before parsing.
Route the status JSON through the shared readProviderJsonResponse, the error
file through readResponseWithLimit, and now the non-OK diagnostic body through
readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the
stream on overflow before decode/parse. assertVoyageResponseOk preserves its
original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies
and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK
output-file branch now reuses it instead of a duplicate unbounded read. The
existing error-file fail-soft handling (formatUnavailableBatchError) is
preserved, so a capped endpoint degrades gracefully. The submit path already
bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched.
Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
Exa search success responses were read via an unbounded `await response.json()`, so a misbehaving or hostile endpoint could stream an arbitrarily large body into memory before parsing. Read the success body through the shared bounded reader (16 MiB cap, the same limit other bundled providers use) and cancel the stream on overflow. This mirrors the error-body bound already in place and the openclaw#95103/openclaw#95108 response -limit campaign on the success-JSON side. AI-assisted.
#96608) The batch status read (fetchVoyageBatchStatus) parsed its response with an unbounded await res.json(), and the batch error-file read (readVoyageBatchError) buffered the whole body via await res.text(). On top of that, the non-OK (4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did await res.text() before throwing, and the non-OK output-file branch in runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and reachable via SSRF, so a misbehaving or hostile endpoint could stream an unbounded body into memory on any of these paths before parsing. Route the status JSON through the shared readProviderJsonResponse, the error file through readResponseWithLimit, and now the non-OK diagnostic body through readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the stream on overflow before decode/parse. assertVoyageResponseOk preserves its original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK output-file branch now reuses it instead of a duplicate unbounded read. The existing error-file fail-soft handling (formatUnavailableBatchError) is preserved, so a capped endpoint degrades gracefully. The submit path already bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched. Symmetric counterpart to the #96027/#96038 response-limit campaign.
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the #96027 / #96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
openclaw#96608) The batch status read (fetchVoyageBatchStatus) parsed its response with an unbounded await res.json(), and the batch error-file read (readVoyageBatchError) buffered the whole body via await res.text(). On top of that, the non-OK (4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did await res.text() before throwing, and the non-OK output-file branch in runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and reachable via SSRF, so a misbehaving or hostile endpoint could stream an unbounded body into memory on any of these paths before parsing. Route the status JSON through the shared readProviderJsonResponse, the error file through readResponseWithLimit, and now the non-OK diagnostic body through readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the stream on overflow before decode/parse. assertVoyageResponseOk preserves its original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK output-file branch now reuses it instead of a duplicate unbounded read. The existing error-file fail-soft handling (formatUnavailableBatchError) is preserved, so a capped endpoint degrades gracefully. The submit path already bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched. Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse (#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's #96042, #96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
Exa search success responses were read via an unbounded `await response.json()`, so a misbehaving or hostile endpoint could stream an arbitrarily large body into memory before parsing. Read the success body through the shared bounded reader (16 MiB cap, the same limit other bundled providers use) and cancel the stream on overflow. This mirrors the error-body bound already in place and the openclaw#95103/openclaw#95108 response -limit campaign on the success-JSON side. AI-assisted.
openclaw#96608) The batch status read (fetchVoyageBatchStatus) parsed its response with an unbounded await res.json(), and the batch error-file read (readVoyageBatchError) buffered the whole body via await res.text(). On top of that, the non-OK (4xx/5xx) diagnostic body was still read unbounded: assertVoyageResponseOk did await res.text() before throwing, and the non-OK output-file branch in runVoyageEmbeddingBatches did the same. Voyage base URLs are user-supplied and reachable via SSRF, so a misbehaving or hostile endpoint could stream an unbounded body into memory on any of these paths before parsing. Route the status JSON through the shared readProviderJsonResponse, the error file through readResponseWithLimit, and now the non-OK diagnostic body through readResponseWithLimit as well, all under a single 16 MiB cap, cancelling the stream on overflow before decode/parse. assertVoyageResponseOk preserves its original "${context}: ${status} ${text}" diagnostic shape for under-cap bodies and throws a bounded "(error body exceeds <N> bytes)" on overflow; the non-OK output-file branch now reuses it instead of a duplicate unbounded read. The existing error-file fail-soft handling (formatUnavailableBatchError) is preserved, so a capped endpoint degrades gracefully. The submit path already bounds its body via postJsonWithRetry/maxResponseBytes and is left untouched. Symmetric counterpart to the openclaw#96027/openclaw#96038 response-limit campaign.
The Copilot usage read in extensions/github-copilot/usage.ts parsed its HTTP response with an unbounded await res.json(). A hostile or buggy api.github.com proxy (the proxy endpoint is derived from a user-supplied token) could stream an unbounded JSON body and drive the usage snapshot into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap, cancels the stream on overflow, and wraps malformed JSON with the caller label. Same no-helper-import-to-bounded-reader shape as the openclaw#96027 / openclaw#96038 response-limit work. Add a focused regression test: when the usage stream exceeds the JSON byte cap, fetchCopilotUsage rejects with a bounded-overflow error and the reader cancels the body mid-flight instead of buffering the full advertised stream. Existing parse/HTTP-error cases keep passing.
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]> (cherry picked from commit 4f3d81b)
What Problem This Solves
The Exa web-search provider parses its success response with an unbounded
await response.json().response.json()reads the entire body into memorybefore parsing, with no byte ceiling and regardless of (or in the absence of) a
Content-Lengthheader. Exa is an external, untrusted source on theweb_searchpath: a misbehaving, compromised, or hostile endpoint (including aself-hosted
baseUrloverride) can stream an arbitrarily large or never-endingJSON body and force the gateway/plugin to buffer it all, causing memory pressure
or a hang on the search code path.
The error-body read in this same file is already bounded
(
readResponseTextLimited, 8 KiB) — the success-JSON read was the remainingunbounded surface. This change closes it, making this the success-JSON-side
symmetric companion to the
#95103/#95108response-limit campaign (and tothe already-merged
readProviderJsonResponsecap in#95218).Changes
readExaSearchResultsnow reads the success body through the shared boundedreader
readResponseWithLimit(re-exported viaopenclaw/plugin-sdk/response-limit-runtime) with a 16 MiB cap, matching thelimit other bundled providers use, then
JSON.parses the decoded prefix.error (
Exa API response exceeds <N> bytes); the existing malformed-JSONerror message is preserved for backward compatibility.
{ maxBytes }argument so tests canexercise the boundary with a small limit.
media-corebounded reader alreadyused across the codebase.
Real behavior proof
Content-Lengthand an oversized/never-ending body must not be bufferedwhole; the read must stop at the cap, cancel the stream, and throw a bounded
error — while valid small responses still parse unchanged.
node:httpserver (createServer) boundto
127.0.0.1, streaming a JSON body in 64 KiB chunks with noContent-Length header, driving the real exported
readExaSearchResultsfunction (imported through the runtime module, plugin-sdk aliases resolved) on
Node v22.22.0.
GET /hugestreams an unbounded JSON object with noContent-Length;GET /smallreturns one valid result.readExaSearchResults(response, { maxBytes: 1 MiB })against/hugeand assert it rejects + the server socket is closed early.
await response.json()against thesame
/hugebody and measure how many bytes the server pushed.readExaSearchResults(response)against/smalland assert theresult is parsed intact.
Exa API response exceeds 1048576 bytes; server socketclosed early (stream cancelled); only 1,114,175 bytes reached the wire —
near the 1 MiB cap, not the 24 MiB body.
response.json()read pulled the full25,166,988 bytes (>22x the bounded read), proving the cap is load-bearing.
urlintact, no truncation.ALL PROOF ASSERTIONS PASSED. Plus the in-repo Vitestsuite for this provider passes 13/13 (including a streaming-fixture regression
test that asserts the oversized read stops before consuming all chunks, a
well-formed-under-cap test, and the existing malformed-JSON / error-body
tests).
oxlintandtsgo(extensions project) are clean on the changedfiles.
api.exa.aiendpoint (no key/ would not reproduce a hostile oversized body); the proof uses a local
server reproducing the exact untrusted-body shape. The 64-bit memory-exhaustion
end state was not driven to OOM — the proof instead measures bytes-on-wire and
early socket close, which is the load-bearing signal.
Evidence
Label: security
AI-assisted.