fix(speech): bound TTS/STT voice-list and transcription JSON responses#96496
Conversation
… reads Route success JSON reads through readProviderJsonResponse (16 MiB cap) in azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and openrouter/media-understanding to prevent OOM from oversized or hostile endpoint responses. Mirrors the response-limit campaign already applied to other provider paths. AI-assisted. Co-authored-by: Cursor <[email protected]>
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 12:38 PM ET / 16:38 UTC. Summary PR surface: Source +21, Tests +2. Total +23 across 7 files. Reproducibility: yes. from source: current main directly uses unbounded 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. Rank-up moves:
Next step before merge
Security Review detailsBest possible solution: Land a focused provider-response hardening change that keeps the existing parsing logic but routes provider-controlled success JSON bodies through the shared bounded SDK helper. Do we have a high-confidence way to reproduce the issue? Yes from source: current main directly uses unbounded Is this the best way to solve the issue? Yes: reusing the existing public plugin SDK bounded JSON helper is the narrowest maintainable fix; provider-local readers would duplicate the already tested contract. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against f29dbd3ebd1c. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +21, Tests +2. Total +23 across 7 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 maintainer review. This is the best narrow fix for the remaining TTS/STT voice-list and transcription success-body reads: it replaces the exact unbounded Local proof: .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main
git diff --check origin/main...origin/pr/96496Focused tests passed locally for the changed provider paths: Azure Speech TTS, ElevenLabs speech provider, and the provider shard covering Microsoft, MiniMax, OpenRouter, and xAI. Autoreview is clean with no accepted/actionable findings, current CI is green, and GitHub reports the PR mergeable. I do not see a breaking-change or compatibility concern for supported TTS/STT behavior; the only changed behavior is the intended fail-fast cap for oversized/runaway successful provider JSON bodies using the existing shared provider JSON limit. |
… reads (openclaw#96496) Route success JSON reads through readProviderJsonResponse (16 MiB cap) in azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and openrouter/media-understanding to prevent OOM from oversized or hostile endpoint responses. Mirrors the response-limit campaign already applied to other provider paths. AI-assisted. Co-authored-by: Cursor <[email protected]>
… reads (openclaw#96496) Route success JSON reads through readProviderJsonResponse (16 MiB cap) in azure-speech, elevenlabs, microsoft, minimax/tts, xai/stt, and openrouter/media-understanding to prevent OOM from oversized or hostile endpoint responses. Mirrors the response-limit campaign already applied to other provider paths. AI-assisted. Co-authored-by: Cursor <[email protected]>
What Problem This Solves
Six TTS/STT providers parse their success responses — voice-list catalog calls
and audio transcription results — with an unbounded
await response.json().response.json()reads the entire body into memory before parsing, with nobyte ceiling and regardless of (or in the absence of) a
Content-Lengthheader.These providers are external, untrusted sources: a misbehaving, compromised, or
hostile endpoint (including a self-hosted
baseUrloverride) can stream anarbitrarily large or never-ending JSON body and force the gateway/plugin to
buffer it all, causing memory pressure or a hang on the TTS/STT code path.
The affected call sites are:
extensions/azure-speech/tts.ts— Azure Speech voice-list catalogextensions/microsoft/speech-provider.ts— Microsoft Azure speech voice-list catalogextensions/elevenlabs/speech-provider.ts— ElevenLabs voice-list catalogextensions/minimax/tts.ts— MiniMax TTS synthesis responseextensions/xai/stt.ts— xAI audio transcription resultextensions/openrouter/media-understanding-provider.ts— OpenRouter STT transcription resultThis change closes all six unbounded surfaces, making this the TTS/STT companion
to the
#95103/#95108/#95218response-limit campaign.Changes
shared bounded reader
readProviderJsonResponse(fromopenclaw/plugin-sdk/provider-http), which enforces a 16 MiB cap andcalls the shared
readResponseWithLimithelper internally.error (
<label>: JSON response exceeds <N> bytes); existingmalformed-JSON and HTTP error paths are preserved for backward compatibility.
media-corebounded reader alreadyused across the codebase.
extensions/openrouter/media-understanding-provider.test.ts, which declaresa closed
vi.mock("openclaw/plugin-sdk/provider-http")factory, receives apass-through mock for
readProviderJsonResponseso existing transcriptionfixture tests remain valid;
extensions/xai/stt.test.tsusesimportOriginalspread and requires no change.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
readResponseWithLimithelper (the same helperreadProviderJsonResponsewraps internally) on Node v22.22.0.
GET /hugestreams an unbounded JSON object (~24 MiB) withno
Content-Length;GET /smallreturns one valid response.readResponseWithLimit(response, 1 MiB, { onOverflow: ... })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.readResponseWithLimit(response, 1 MiB)against/smalland assertthe result is parsed intact.
JSON response exceeds 1048576 bytes; server socketclosed early (stream cancelled); only 1,064,178 bytes reached the
wire — near the 1 MiB cap, not the 24 MiB body.
response.json()read pulled the full25,165,824 bytes (>23x the bounded read), proving the cap is
load-bearing.
ALL PROOF ASSERTIONS PASSED. Plus the in-repo Vitestsuite for the two providers with closed mocks passes — 2/2 files,
12/12 tests — including existing transcription fixture tests.
oxlintisclean on changed files.
not reproduce a hostile oversized body). 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.