Skip to content

fix: add resource limit in zen-proxy.mjs (CWE-400) - #6

Merged
12errh merged 2 commits into
12errh:mainfrom
anupamme:fix-repo-zen-proxy-cwe-400-readbody-size-limit
Oct 9, 2026
Merged

12errh merged 2 commits into
12errh:mainfrom
anupamme:fix-repo-zen-proxy-cwe-400-readbody-size-limit

Conversation

@anupamme

@anupamme anupamme commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

The readBody function reads the entire request body into memory without enforcing a size limit during streaming. While handleChat checks raw.length > MAX_BODY after reading, the entire payload has already been buffered in memory, allowing memory exhaustion before the size check can reject it. The affected code is zen-proxy.mjs:530. This change is the fix I would apply.

Reference: CWE-400

What changed

  • zen-proxy.mjs: readBody now takes a limit (default MAX_BODY) and throws mid-stream — as soon as the running total exceeds the limit — instead of buffering the whole body and checking its length afterward. Since readBody is shared by handleChat, handleApiConfig (PUT), and handleTest, all three are protected by this one change. handleApiConfig (PUT) and handleTest now also return a precise 413 for the oversized-body case (previously a generic 400), matching handleChat.
  • test/zen-proxy.test.mjs: added a chunk-tracking request mock (mockStreamingReq) and regression tests proving the request is aborted mid-stream (the handler stops pulling chunks before the generator is drained) rather than after full buffering, across handleChat, handleApiConfig (PUT), and handleTest. Also added MAX_BODY boundary tests (exactly at the limit, one byte over, one byte under).

Verification

npm test — 130 passed, 1 skipped (unrelated: pwsh not available on this host for an install.ps1 test). As a sanity check on the new tests themselves, I temporarily reverted readBody to the original unbounded form and confirmed the new mid-stream tests fail specifically on the "stream not fully drained" assertion (not just the status code) — confirming they exercise the actual security property, not just the final response.


Automated security fix by OrbisAI Security

Automated security fix generated by OrbisAI Security
@tirforge

tirforge commented Oct 7, 2026

Copy link
Copy Markdown

Heads-up: this is already covered (and extended) in PR #9 — mid-stream body-limit enforcement with early abort exists there as readBodyLimited (413 BODY_TOO_LARGE) and is applied to all body-reading routes (chat, messages, responses, config POST/PUT, /api/test), not just handleChat. Merging this as-is would conflict; suggest closing in favor of #9 or rebasing onto it.

…n routes

readBody() already aborts mid-stream once a request exceeds MAX_BODY (see
previous commit), but the existing oversized-body test only checks the final
status, so it can't distinguish a true early abort from the old
buffer-everything-then-check pattern. Add a chunk-tracking request mock
(mockStreamingReq) and tests that assert the handler stops consuming chunks
before the stream is drained, across handleChat, handleApiConfig (PUT), and
handleTest.

Also give handleApiConfig (PUT) and handleTest the same precise 413 response
handleChat already has for oversized bodies, instead of a generic 400 — all
three share the same readBody() primitive, so the memory-safety property was
already in place; this just makes the response shape consistent.
@anupamme

anupamme commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for flagging #9 — I checked its actual diff rather than taking the summary at face value, and the coverage claim doesn't hold up for the three primary API routes. Citing exact lines from the current PR #9 branch (tirforge/zen-proxy@main, zen-proxy.mjs):

  • readBody (unbounded, unchanged): line 755
  • readBodyLimited (new, mid-stream abort): line 763; bodyTooLarge: line 777
  • handleChat (line 1558) calls plain readBody(req) at line 1562 — not mid-stream limited
  • handleResponses (line 1768) calls plain readBody(req) at line 1775 — not mid-stream limited
  • handleMessages (line 1967) calls plain readBody(req) at line 1971 — not mid-stream limited
  • handleApiConfig (line 2578) calls readBodyLimited at 2585/2602 — limited
  • handleTest (line 2701) calls readBodyLimited at 2704 — limited

So readBodyLimited is wired into exactly two admin routes (/api/config, /api/test). The three highest-traffic routes — /v1/chat/completions, /v1/messages, /v1/responses — still buffer the entire body via the original readBody and only check raw.length > MAX_BODY after the fact. That's the exact CWE-400 pattern this PR exists to fix, just now duplicated into two more handlers by #9 rather than closed.

I'd rather not close this one in favor of #9 as-is, since #9 doesn't actually resolve the finding for the primary request path. A couple reasons I think this PR's approach is also preferable on the merits, not just on coverage:

  • It fixes the shared readBody primitive itself (limit param, throws mid-stream), so every current caller (handleChat, handleApiConfig, handleTest) — and any future one — is protected automatically. Render support, Anthropic API, 2.x emulation, streaming hardening #9's readBodyLimited requires each handler to remember to opt in, which is exactly how three routes ended up missed.
  • I've added regression tests (just pushed) that specifically prove the mid-stream property — a chunk-tracking mock asserts the handler stops pulling chunks before the stream is drained, not just that the final status is 413. The pre-existing single-chunk test couldn't distinguish "aborted mid-stream" from "buffered everything, then rejected." I also extended precise 413 responses to handleApiConfig (PUT) and handleTest, which previously fell through to a generic 400 for the same oversized-body case.

Suggested path: merge this fix first (it's small and now has test coverage), and have #9 rebase onto it — dropping the now-redundant readBodyLimited/bodyTooLarge in favor of the shared readBody(req, limit). Alternatively, if #9 lands first, it should extend mid-stream enforcement to handleChat/handleMessages/handleResponses before merging, since otherwise the original finding stays open on the primary API surface.

Happy to rebase this PR onto #9 once that's settled either way.

@12errh
12errh merged commit 5d1537a into 12errh:main Oct 9, 2026
5 checks passed
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.

3 participants