SEC2-050: postulo-mcp locks mcp 2.1.1, whose Streamable HTTP server keeps sessions without bound (CVE-2026-59951), and CI never audits it #17

Closed
opened 2026-10-17 10:39:31 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo-mcp at 3819dbd, checked by a second, independent pass.

Severity: Low · Category: Dependencies · Reference: CWE-400 (Uncontrolled Resource Consumption); CWE-1104 (Use of Unmaintained Third Party Components); OWASP A06:2021 (Vulnerable and Outdated Components)

Location:

  • uv.lock (mcp 2.1.1, pyjwt 2.13.0)
  • pyproject.toml:18 (dependencies = ["mcp>=2.0", "httpx>=0.28"])
  • src/postulo_mcp/serving.py:32 (HTTP_TRANSPORTS = ("sse", "streamable-http")), src/postulo_mcp/serving.py:135-137 (run_arguments: host, port and transport_security only)
  • src/postulo_mcp/__main__.py:45-48,130 (--transport choices; server.run(transport=...))
  • .forgejo/workflows/ci.yml:20-27 (unversioned installer; uv sync without --locked; no audit step)

Issue

Unlike the other plugins, postulo-mcp is its own process ("Postulo itself is not a dependency at all", pyproject.toml:16-18), so its lock is what runs. A pip-audit 2.10.1 run on 2026-10-16 against the exported lock reports mcp 2.1.1 with CVE-2026-59951 (GHSA-84m7-p3x7-pcfv, published 2026-10-14, fixed in 1.30.0 / 2.2.0): a Streamable HTTP server in stateful mode (the default of MCPServer.run()) never reclaimed session state, registered a session even when its first request was refused, and had no cap on the number of sessions, so a client that can reach the endpoint can grow server memory without bound; the advisory says that authentication limits who can open sessions, not how many. postulo-mcp offers that transport (--transport streamable-http) and passes no stateless_http=True. The default transport is stdio, which is not affected, so the exposure needs an operator who chose to listen on a port; serving.py:3-7 already treats that as the dangerous configuration.

The lock also holds pyjwt 2.13.0 (fourteen advisories, fixed in 2.14.0 / 2.15.0), pulled in by mcp's auth module. postulo-mcp verifies its own shared secret with SharedSecret.verify_token (serving.py:73-76, a compare_digest) and decodes no JWT, so that one is minor on its own.

Impact

Anyone who can reach the port of an instance started with --transport streamable-http can exhaust the server's memory with unauthenticated or refused requests until it is restarted, taking the agent's access to the job search with it.

Fix

Nothing in server.py keeps per-session state, so make run_arguments() return stateless_http=True for streamable-http (mcp 2.1.1 already accepts it, so the mitigation does not wait for the upgrade), and lower max_request_body_size for a personal host. Then uv lock --upgrade-package mcp to the current release (>= 2.2.0), which brings pyjwt >= 2.15.1 through pyjwt[crypto]. Done when: pip-audit against the exported lock reports nothing, and a test asserts that plan("streamable-http", ...).run_arguments() carries stateless_http=True.

#8 (open) - CI image, --locked and the Python matrix: the CI half (uv sync --locked, pinned installer) belongs there. postulo/postulo-apprise#13 (open) - names this repository's ci.yml:22 installer line and missing audit step. #3 (closed) - "no unauthenticated HTTP transport", the shared secret the advisory says does not bound session count.

Found on 2026-10-16 in a read-only security review of `postulo-mcp` at `3819dbd`, checked by a second, independent pass. **Severity:** Low · **Category:** Dependencies · **Reference:** CWE-400 (Uncontrolled Resource Consumption); CWE-1104 (Use of Unmaintained Third Party Components); OWASP A06:2021 (Vulnerable and Outdated Components) **Location:** - `uv.lock` (`mcp 2.1.1`, `pyjwt 2.13.0`) - `pyproject.toml:18` (`dependencies = ["mcp>=2.0", "httpx>=0.28"]`) - `src/postulo_mcp/serving.py:32` (`HTTP_TRANSPORTS = ("sse", "streamable-http")`), `src/postulo_mcp/serving.py:135-137` (`run_arguments`: host, port and `transport_security` only) - `src/postulo_mcp/__main__.py:45-48,130` (`--transport` choices; `server.run(transport=...)`) - `.forgejo/workflows/ci.yml:20-27` (unversioned installer; `uv sync` without `--locked`; no audit step) ## Issue Unlike the other plugins, postulo-mcp is its own process ("Postulo itself is not a dependency at all", `pyproject.toml:16-18`), so its lock is what runs. A pip-audit 2.10.1 run on 2026-10-16 against the exported lock reports `mcp 2.1.1` with CVE-2026-59951 (GHSA-84m7-p3x7-pcfv, published 2026-10-14, fixed in 1.30.0 / 2.2.0): a Streamable HTTP server in stateful mode (the default of `MCPServer.run()`) never reclaimed session state, registered a session even when its first request was refused, and had no cap on the number of sessions, so a client that can reach the endpoint can grow server memory without bound; the advisory says that authentication limits who can open sessions, not how many. postulo-mcp offers that transport (`--transport streamable-http`) and passes no `stateless_http=True`. The default transport is stdio, which is not affected, so the exposure needs an operator who chose to listen on a port; `serving.py:3-7` already treats that as the dangerous configuration. The lock also holds `pyjwt 2.13.0` (fourteen advisories, fixed in 2.14.0 / 2.15.0), pulled in by `mcp`'s auth module. postulo-mcp verifies its own shared secret with `SharedSecret.verify_token` (`serving.py:73-76`, a `compare_digest`) and decodes no JWT, so that one is minor on its own. ## Impact Anyone who can reach the port of an instance started with `--transport streamable-http` can exhaust the server's memory with unauthenticated or refused requests until it is restarted, taking the agent's access to the job search with it. ## Fix Nothing in `server.py` keeps per-session state, so make `run_arguments()` return `stateless_http=True` for `streamable-http` (mcp 2.1.1 already accepts it, so the mitigation does not wait for the upgrade), and lower `max_request_body_size` for a personal host. Then `uv lock --upgrade-package mcp` to the current release (>= 2.2.0), which brings pyjwt >= 2.15.1 through `pyjwt[crypto]`. Done when: pip-audit against the exported lock reports nothing, and a test asserts that `plan("streamable-http", ...).run_arguments()` carries `stateless_http=True`. ## Related #8 (open) - CI image, `--locked` and the Python matrix: the CI half (`uv sync --locked`, pinned installer) belongs there. postulo/postulo-apprise#13 (open) - names this repository's `ci.yml:22` installer line and missing audit step. #3 (closed) - "no unauthenticated HTTP transport", the shared secret the advisory says does not bound session count.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-mcp#17
No description provided.