SEC2-066: Postulo._call reads every answer whole, with no size ceiling and no deadline for the whole exchange #18
Labels
No labels
bug
documentation
enhancement
security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-mcp#18
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-10-16 in a read-only security review of
postulo-mcpat3819dbd, checked by a second, independent pass.Severity: Informational · Category: Input Validation · Reference: CWE-400 (Uncontrolled Resource Consumption); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo_mcp/client.py:19(TIMEOUT = 20.0)src/postulo_mcp/client.py:74-85(httpx.Client(... timeout=TIMEOUT ...))src/postulo_mcp/client.py:112-131(_call:response = self.client.request(...),response.json(),response.text)Issue
The client is built with a single float timeout and reads bodies the default way:
timeout=20.0bounds each wait (connect, one read, one write), not their sum, andclient.requesthas read the whole body before it returns. Nothing caps the body, and a checkof
Content-Lengthafterrequest()would come too late for the same reason. The core closedthe same two gaps in its own outbound client (
plugins/http.py:total=andread_body(limit=), postulo/postulo#321, #781), whose docstring explains that "a size limitapplied to what [
client.get] hands back limits nothing" and that "a server sending a byte everyfew seconds never trips [a per-wait timeout]".
The answerer is normally the person's own instance, but
POSTULO_URLmay point at a reverseproxy, a forward-auth portal or a mistyped host (the cases #7 describes), and over the HTTP
transports the server runs unattended.
Impact
Whatever answers at
POSTULO_URLcan make one tool call hold the MCP server (and the agentwaiting on it) for as long as it likes by dripping bytes inside the 20 s read timeout, or make
it buffer an arbitrarily large body in memory. No data is exposed. On stdio that is the person's
own session; on
--transport streamable-httpit is a server other clients share.Fix
In
_call, read the body streamed, under a ceiling and a deadline for the whole exchange:keeping
httpx.HTTPErrorhandling as it is. The client'stimeout=TIMEOUTstays; the checkinside the loop is what bounds the sum.
Done when
tests/test_server.pyhas aMockTransportthat answers a body larger thanMAX_BODYand one that declares aContent-Lengthover it, and each tool call ends in aPostuloErrorrather than a dictionary.Related
#7 (open) is about a 3xx or an HTML 200 being handed back as success; its fix (refuse non-JSON)
does not bound size or total time. #15 (open) is about which fields of a problem document reach
the agent.