Skip to content

fix(sandbox-proxy): cap upstream response body size on all proxied routes - #5330

Merged
pedrofrxncx merged 1 commit into
mainfrom
fix/sandbox-proxy-cap-upstream-response-size-w3
Jul 28, 2026
Merged

fix(sandbox-proxy): cap upstream response body size on all proxied routes#5330
pedrofrxncx merged 1 commit into
mainfrom
fix/sandbox-proxy-cap-upstream-response-size-w3

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Source: a hardening follow-up to #5316/#5319, found while auditing the same file (apps/api/src/api/routes/sandbox-proxy.ts) for this tick's "sandbox-proxy route robustness" focus area.

Why: #5316 and #5319 capped every route that reads a client request body fully into memory via c.req.text() before forwarding it to the daemon (forwardedBodyLimit, 10MB). The response side of the exact same routes has the identical gap: proxyDaemon's resolveAndFetch reads the daemon's response via upstream.text() with no size ceiling, fetchDaemonJson (used by suggest-commit/judge-review) does the same, and the preview-fetch/preview-invoke routes read a customer's own preview-server response the same unbounded way. A runaway git diff, a chatty exec script writing to stdout, or a large preview page gets buffered entirely into the shared Studio API process's memory — the same DoS-by-memory-exhaustion class #5316/#5319 just closed, just on the other side of the same fetch.

Fix: added readBoundedText(response, maxBytes), which reads a Response body in chunks via its reader and throws UpstreamPayloadTooLargeError once the running total exceeds maxBytes, instead of buffering an unbounded amount. Wired it into all four remaining upstream.text() call sites in this file, reusing the existing FORWARDED_BODY_MAX_BYTES (10MB) constant so the response cap matches the already-established request cap. proxyDaemon's catch block now returns a clear 502 for the new error instead of folding it into the generic "Daemon unreachable" message.

Regression test: readBoundedText returns the full body under the cap and throws UpstreamPayloadTooLargeError (instead of buffering) once a body exceeds it — added to sandbox-proxy.test.ts.

How to verify: bun test apps/api/src/api/routes/sandbox-proxy.test.ts (10 pass, including the 2 new cases); grep -n 'upstream.text()' apps/api/src/api/routes/sandbox-proxy.ts now returns nothing — every prior unbounded read goes through readBoundedText.

Checks run locally: bun run fmt and cd apps/api && bunx tsc --noEmit (both clean), plus the targeted test above. Full CI validates the rest.


Summary by cubic

Cap upstream response body size to 10MB across all sandbox-proxy routes to prevent memory exhaustion. Responses are now read in chunks, with clear 502 errors when the cap is exceeded.

  • Bug Fixes
    • Added readBoundedText(response, maxBytes) that streams and caps response bodies, throwing UpstreamPayloadTooLargeError.
    • Replaced all upstream.text() calls in apps/api/src/api/routes/sandbox-proxy.ts (proxyDaemon, fetchDaemonJson, preview-fetch, preview-invoke) with readBoundedText.
    • Reused FORWARDED_BODY_MAX_BYTES (10MB) to match the existing request body cap.
    • On oversize upstream bodies, return 502 with a specific error instead of the generic "Daemon unreachable".
    • Added tests for capped reads in sandbox-proxy.test.ts.

Written for commit 1e942fe. Summary will update on new commits.

Review in cubic

…utes

#5316/#5319 capped request bodies clients send into this router
(forwardedBodyLimit) because proxyDaemon buffers them fully into memory
via c.req.text() before forwarding. The response side had the same gap:
every route reads the daemon's (or, for preview-fetch/preview-invoke, the
customer's own preview server's) response fully via upstream.text() with
no ceiling. A runaway git diff, a chatty exec script, or a huge preview
page gets buffered entirely into the Studio API process's memory.

Added readBoundedText(), which reads a Response body in chunks and throws
UpstreamPayloadTooLargeError once it exceeds the same 10MB cap already
used for request bodies, instead of buffering an unbounded amount. Wired
it into proxyDaemon, fetchDaemonJson (used by suggest-commit/judge-review),
and the preview-fetch/preview-invoke routes — replacing every remaining
upstream.text() call in this file.
@pedrofrxncx
pedrofrxncx enabled auto-merge (squash) July 28, 2026 15:01
@pedrofrxncx
pedrofrxncx merged commit abd1b8b into main Jul 28, 2026
21 of 22 checks passed
@pedrofrxncx
pedrofrxncx deleted the fix/sandbox-proxy-cap-upstream-response-size-w3 branch July 28, 2026 15:03
decocms Bot pushed a commit that referenced this pull request Jul 28, 2026
PR: #5330 fix(sandbox-proxy): cap upstream response body size on all proxied routes
Bump type: patch

- decocms (apps/api/package.json): 4.139.11 -> 4.139.12

Deploy-Scope: server
pedrofrxncx added a commit that referenced this pull request Jul 28, 2026
apps/api/src/tools/sandbox/{patch-sandbox-operator,resolve-env,sync-git-credentials}.ts
each PUT to the daemon's /_sandbox/config and, on a non-ok response, read the
error body with a plain res.text() with no size cap. sandbox-proxy.ts already
bounds every proxied response to the same endpoint via readBoundedText()
(#5316/#5319/#5330) — these three call sites hit /_sandbox/config directly
(during publish, rebase, and sandbox start) and were missed, so a
compromised or misbehaving daemon can still make the API process buffer an
unbounded error body into memory on that path.

Extracted readBoundedText()/UpstreamPayloadTooLargeError out of
sandbox-proxy.ts into apps/api/src/lib/bounded-text.ts (no behavior change)
so the tools/sandbox call sites can reuse it without an api-routes ->
tools-layer dependency, and switched all three sites to it with the same
10MB cap sandbox-proxy.ts already applies to this endpoint.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant