fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM#96031
Conversation
|
@clawsweeper review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…revent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers).
…mited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length.
d9fca1c to
f9ec561
Compare
|
Codex review: needs maintainer review before merge. Reviewed June 24, 2026, 6:37 AM ET / 10:37 UTC. Summary PR surface: Source +37, Tests +126. Total +163 across 2 files. Reproducibility: yes. Current main is source-reproducible because the Nextcloud Talk send path still buffers three untrusted external response bodies with response.text()/response.json(), and the PR body supplies after-fix terminal proof for the bounded path. 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. Next step before merge
Security Review detailsBest possible solution: Merge this focused bounded-read fix after ordinary maintainer and CI gates, and track any broader response-read audit separately. Do we have a high-confidence way to reproduce the issue? Yes. Current main is source-reproducible because the Nextcloud Talk send path still buffers three untrusted external response bodies with response.text()/response.json(), and the PR body supplies after-fix terminal proof for the bounded path. Is this the best way to solve the issue? Yes. The patch is the narrow maintainable fix for this PR scope: it changes the owning Nextcloud Talk send module, uses existing public SDK helpers, preserves the sent-but-unparseable receipt fallback, and avoids new config or SDK surface. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against bfffc77bfc86. Label changesLabel justifications:
Evidence reviewedPR surface: Source +37, Tests +126. Total +163 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
|
|
Updated the PR body with standalone runtime proof using the exact |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
…uccess JSON
The send success receipt parsed JSON by hand via readResponseWithLimit + a
local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)),
duplicating the shared provider-http helper that the sibling room-info.ts and
bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing
shared helpers locally.
Swap the hand-rolled block for the one-stop
readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which
reads through the same bounded reader and throws on overflow/malformed JSON, so
the outer try/catch still keeps the "unknown" receipt and behavior is
equivalent. The error path keeps readResponseTextLimited (text, not JSON).
|
Merge-ready for 312700b. Local and ClawSweeper review clean, CI green, best narrow fix, and follows the established provider-response-read pattern. The change keeps the Nextcloud Talk external send/reaction response handling scoped to successful REST reads and adds the focused oversized-response cancellation coverage needed for the OOM guardrail. |
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON).
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON).
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON).
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON). (cherry picked from commit d577cb2)
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON).
…revent OOM (openclaw#96031) * fix(nextcloud-talk): bound external send/reaction response reads to prevent OOM Nextcloud Talk talks to self-hosted servers whose HTTP responses are not trusted to be small. The send and reaction paths buffered three external bodies without any byte cap: - success JSON via await response.json() - send error text via await response.text() - reaction error text via await response.text() A hostile or misbehaving Nextcloud endpoint could stream an unbounded body (no content-length) into memory, pressuring or hanging the plugin/provider path. Cap success JSON at 16 MiB via readResponseWithLimit and collapse error bodies to an 8 KiB readResponseTextSnippet, cancelling the stream on overflow. The 'message sent but receipt JSON unreadable -> unknown' fallback is preserved (an over-limit body now also routes through the existing catch). This is the symmetric counterpart to the openclaw#95103/openclaw#95108 response-limit campaign, reusing the shared @openclaw/media-core helpers (newly re-exported from plugin-sdk/response-limit-runtime for plugin consumers). * fix(nextcloud-talk): bound error bodies via public readResponseTextLimited (no new plugin-SDK surface) Re-exporting readResponseTextSnippet from plugin-sdk/response-limit-runtime pushed the public plugin-SDK export count past its surface budget, failing plugin-sdk-surface-report.test.ts. Drop that re-export and instead bound the Nextcloud Talk send/reaction error bodies through the already-public readResponseTextLimited (openclaw/plugin-sdk/provider-http), collapsing the bounded 8 KiB prefix to a short, log-safe snippet locally. Behavior is unchanged for callers; no new plugin-SDK surface is introduced. Success JSON still reads through readResponseWithLimit (16 MiB cap). The committed bounded-response-reads Vitest suite continues to prove the caps hold against 17 MiB streamed bodies with no content-length. * fix(nextcloud-talk): reuse shared readProviderJsonResponse for send success JSON The send success receipt parsed JSON by hand via readResponseWithLimit + a local NEXTCLOUD_TALK_JSON_MAX_BYTES cap + JSON.parse(TextDecoder.decode(...)), duplicating the shared provider-http helper that the sibling room-info.ts and bot-preflight.ts already use. extensions/AGENTS.md forbids re-implementing shared helpers locally. Swap the hand-rolled block for the one-stop readProviderJsonResponse<{ ocs?: ... }>(response, "Nextcloud Talk send"), which reads through the same bounded reader and throws on overflow/malformed JSON, so the outer try/catch still keeps the "unknown" receipt and behavior is equivalent. The error path keeps readResponseTextLimited (text, not JSON). (cherry picked from commit d577cb2)
What Problem This Solves
The Nextcloud Talk channel sends to self-hosted servers, so its HTTP responses are not under our control and are not trusted to be small. Three external response bodies in
send.tswere buffered with no byte cap:await response.json()(every successful send/receipt parse)await response.text()(non-2xx send responses)await response.text()(non-2xx reaction responses)A hostile or misbehaving Nextcloud endpoint can return a response without a
content-lengthheader and stream an unbounded body. With the old code that body was fully buffered into memory, creating memory pressure / a potential hang on the plugin (and the provider/outbound) path — a small reaction error or a bogus 200 body could expand to gigabytes. This is the symmetric counterpart to the#95103/#95108response-limit campaign, which is hardening exactly these unboundedresponse.json()/response.text()reads across channels and providers.Changes
extensions/nextcloud-talk/src/send.tsreadResponseWithLimit(cap 16 MiB, the same cap used for provider JSON in#95218), thenJSON.parseon the decoded prefix. On overflow the stream is cancelled and a bounded error is thrown.readResponseTextLimited(fromopenclaw/plugin-sdk/provider-http, cap 8 KiB, cancels the upstream stream once the byte budget is full), then a small localcollapseErrorSnippetcollapses whitespace and caps the prefix at 200 chars. This is wrapped inreadNextcloudTalkErrorSnippet, which returns""on any read failure (matching the previous.catch(() => "")semantics). No new plugin-SDK surface is added.unknownreceipt" behavior is intact. An over-limit success body now also flows through that sametry/catch, so a hostile success body never throws to the caller — it just yields theunknownreceipt.extensions/nextcloud-talk/src/send.cfg-threading.test.tsbounded response readssuite driving the real exported functions against streamedReadableStreambodies with nocontent-length: over-limit success JSON keeps theunknownreceipt, over-limit send/reaction error bodies stay bounded (error message far below the streamed 17 MiB body), and a normal small body still parses fully.Real behavior proof
Behavior addressed: Nextcloud Talk outbound send success JSON, send error text, and reaction error text are no longer read with unbounded
response.json()/response.text()from a self-hosted server. Oversized no-Content-Lengthstreams are bounded, while normal small success JSON still parses.Real environment tested: Local OpenClaw checkout on Linux, running a standalone
node --import tsx --input-type=moduleharness from the repository root. The harness starts a real localnode:httpserver on127.0.0.1, enableschannels.nextcloud-talk.network.dangerouslyAllowPrivateNetwork, and drives the real exportedsendMessageNextcloudTalk/sendReactionNextcloudTalkfunctions through the realfetchWithSsrFGuardpath.Exact steps or command run after this patch: The harness exercised four paths against live HTTP responses with no
Content-Length: a 17 MiB streamed 200 success body, a normal small 200 success JSON body, a 17 MiB streamed 400 send error body, and a 17 MiB streamed 500 reaction error body.Evidence after fix:
Observed result after fix: The over-cap success body no longer throws to the caller and preserves the existing
messageId="unknown"fallback; small JSON still returnsmessageId="99"and the timestamp; send/reaction error messages remain short (231/237chars) instead of embedding streamed multi-MiB bodies.What was not tested: A live production Nextcloud Talk deployment. The proof uses a local HTTP server because it must deterministically stream oversized no-
Content-Lengthbodies; the exercised code path still uses the real exported send/reaction functions and real SSRF-guarded fetch path.Evidence
Label: security
AI-assisted. This is the symmetric continuation of the
#95103/#95108response-limit campaign, applying the same@openclaw/media-corereadResponseWithLimithardening to the Nextcloud Talk success JSON, and bounding the send/reaction error bodies through the already-publicreadResponseTextLimitedplus a local snippet collapse (no new plugin-SDK surface).🤖 Generated with Claude Code