Request bodies have no size limit, so one operator request can exhaust controller memory #94

Closed
opened 2026-10-09 15:31:06 -04:00 by goddard · 1 comment
Collaborator

The controller accepts request bodies of any size, so a single operator request can exhaust its memory and take down every background worker with it.

  • Dockerfile:35 and docker-compose.yml:20 set BODY_SIZE_LIMIT=Infinity, which turns off adapter-node's body cap for the whole app.
  • The auth, agent and game-file routes use bounded readers (authBody, objectBody, 8 MiB upload chunks). The rest buffer the whole body:
    • api/scripts/+server.ts:38,59, scripts/[id]/versions/+server.ts:25,30, scripts/inspect/+server.ts:7,12 and betas/[id]/revisions/+server.ts:10,18 call request.formData() and then arrayBuffer()
    • 31 request.json() calls across 29 route files, such as servers/+server.ts:10, provisioning/+server.ts:25 and plugins/github/import/+server.ts:8

native-hosts.md:661-662 says "application routes still enforce these limits", which isn't true for these. With port 3000 published directly (#67), a proxy limit doesn't cover it either. The agent also rejects files over 32 MiB (ec_agent.py:40), so an oversized artifact uploads fine and only fails at deploy time.

Fix

  • Read JSON bodies through a bounded reader (authBody or objectBody)
  • Check Content-Length and count bytes before calling formData(), and cap artifacts at the agent's 32 MiB
  • Correct native-hosts.md:661-662
The controller accepts request bodies of any size, so a single operator request can exhaust its memory and take down every background worker with it. - `Dockerfile:35` and `docker-compose.yml:20` set `BODY_SIZE_LIMIT=Infinity`, which turns off adapter-node's body cap for the whole app. - The auth, agent and game-file routes use bounded readers (`authBody`, `objectBody`, 8 MiB upload chunks). The rest buffer the whole body: - `api/scripts/+server.ts:38,59`, `scripts/[id]/versions/+server.ts:25,30`, `scripts/inspect/+server.ts:7,12` and `betas/[id]/revisions/+server.ts:10,18` call `request.formData()` and then `arrayBuffer()` - 31 `request.json()` calls across 29 route files, such as `servers/+server.ts:10`, `provisioning/+server.ts:25` and `plugins/github/import/+server.ts:8` `native-hosts.md:661-662` says "application routes still enforce these limits", which isn't true for these. With port 3000 published directly (#67), a proxy limit doesn't cover it either. The agent also rejects files over 32 MiB (`ec_agent.py:40`), so an oversized artifact uploads fine and only fails at deploy time. **Fix** - [x] Read JSON bodies through a bounded reader (`authBody` or `objectBody`) - [x] Check `Content-Length` and count bytes before calling `formData()`, and cap artifacts at the agent's 32 MiB - [x] Correct `native-hosts.md:661-662`
goddard added the
priority
low
type/hardening
labels 2026-10-09 15:31:06 -04:00
josh was assigned by goddard 2026-10-09 15:31:06 -04:00
Author
Collaborator

Reviewed 9eb3aa84c3. Every route that reads a body now has a size limit, artifacts are capped at the agent's 32 MiB, and the docs match, so I'm closing this.

Checklist

  • Read JSON bodies through a bounded reader. The new request-body.ts adds jsonBody, with a 1 MiB default. It refuses an oversized Content-Length before reading, counts the bytes it actually receives, and copies them into 64 KiB blocks so many tiny chunks can't inflate memory. No request.json(), formData(), text() or arrayBuffer() calls on the request remain anywhere under src. Routes that turned bad JSON into an empty object or 400 now re-throw 413 first. request-body-routes.test.ts sends declared and undeclared oversized bodies to all 31 JSON routes and expects 413 from each. It also checks that malformed JSON still gets 400.
  • Check size before formData() and cap artifacts at 32 MiB. artifactFormBody buffers at most 33 MiB (65 MiB for beta revisions) before it parses anything, then rejects any file or text field over MAX_ARTIFACT_BYTES (32 MiB, matching the agent's MAX_FILE). createScript and uploadNewVersion enforce the same cap for non-form callers, and the GitHub import now uses it in place of 64 MiB. Tests cover:
    • a 32 MiB file plus form overhead is accepted, and 1 byte more is refused, for both file and text fields
    • the beta revision cap holds even without a declared length
    • all six upload routes refuse an oversized declared body before any version is written
  • Correct native-hosts.md:661-662. The "Proxy limits and older uploads" section (native-hosts.md:658) now lists each route's real limit. It explains why BODY_SIZE_LIMIT=Infinity stays for legacy source-host imports, and it raises the suggested proxy limit to 128 MiB to cover agent operation results. plugin-releases.md now says 32 MiB.

Review notes

  • Good extra catch: telemetry goes through jsonBody too, which covers the first fix item of #74.
  • The 5 body-limit, plugin-import, script and guard test files pass locally (142 tests), and CI run 838 passed on this commit.
  • Minor and non-blocking: an upload is still held in memory about three times over: the bounded body, the parsed form, then Buffer.from(await file.arrayBuffer()) (scripts/+server.ts:63, scripts/[id]/versions/+server.ts:38, betas/[id]/revisions/+server.ts:24). That's roughly 100 MiB per maximum-size upload. It's bounded per request, but not across simultaneous uploads.
Reviewed 9eb3aa84c3f2e6b2a7d18880c3bc53cd218f8852. Every route that reads a body now has a size limit, artifacts are capped at the agent's 32 MiB, and the docs match, so I'm closing this. **Checklist** - [x] **Read JSON bodies through a bounded reader.** The new `request-body.ts` adds `jsonBody`, with a 1 MiB default. It refuses an oversized `Content-Length` before reading, counts the bytes it actually receives, and copies them into 64 KiB blocks so many tiny chunks can't inflate memory. No `request.json()`, `formData()`, `text()` or `arrayBuffer()` calls on the request remain anywhere under `src`. Routes that turned bad JSON into an empty object or 400 now re-throw 413 first. `request-body-routes.test.ts` sends declared and undeclared oversized bodies to all 31 JSON routes and expects 413 from each. It also checks that malformed JSON still gets 400. - [x] **Check size before `formData()` and cap artifacts at 32 MiB.** `artifactFormBody` buffers at most 33 MiB (65 MiB for beta revisions) before it parses anything, then rejects any file or text field over `MAX_ARTIFACT_BYTES` (32 MiB, matching the agent's `MAX_FILE`). `createScript` and `uploadNewVersion` enforce the same cap for non-form callers, and the GitHub import now uses it in place of 64 MiB. Tests cover: - a 32 MiB file plus form overhead is accepted, and 1 byte more is refused, for both file and text fields - the beta revision cap holds even without a declared length - all six upload routes refuse an oversized declared body before any version is written - [x] **Correct `native-hosts.md:661-662`.** The "Proxy limits and older uploads" section (`native-hosts.md:658`) now lists each route's real limit. It explains why `BODY_SIZE_LIMIT=Infinity` stays for legacy source-host imports, and it raises the suggested proxy limit to 128 MiB to cover agent operation results. `plugin-releases.md` now says 32 MiB. **Review notes** - Good extra catch: telemetry goes through `jsonBody` too, which covers the first fix item of #74. - The 5 body-limit, plugin-import, script and guard test files pass locally (142 tests), and CI [run 838](https://gitea.thewrightserver.net/josh/electric-cherry/actions/runs/838) passed on this commit. - Minor and non-blocking: an upload is still held in memory about three times over: the bounded body, the parsed form, then `Buffer.from(await file.arrayBuffer())` (`scripts/+server.ts:63`, `scripts/[id]/versions/+server.ts:38`, `betas/[id]/revisions/+server.ts:24`). That's roughly 100 MiB per maximum-size upload. It's bounded per request, but not across simultaneous uploads.
Sign in to join this conversation.