Upgrade FastMCP to 3.2.4 / MCP to 1.27.0 for CVE-2026-32871

Bump FastMCP from 2.13.1 to 3.2.4 and MCP from 1.22.0 to 1.27.0 to remediate CVE-2026-32871 (GHSA-vv7q-7jx5-f767), as flagged by Docker Scout. Add a regression test covering OpenAPI path-parameter escaping so malicious values like ../../../admin/delete-all? remain percent-encoded under the intended route prefix instead of resolving to a different backend path. Validation: - smoke-tested Agent Zero MCP initialization against fastmcp 3.2.4 + mcp 1.27.0 - PYTHONPATH=/tmp/agent-zero-testdeps python3 -m pytest tests/test_fastmcp_openapi_security.py -q Refs: - CVE-2026-32871 - Docker Scout: https://scout.docker.com/vulnerabilities/id/CVE-2026-32871 - GitHub advisory: https://github.com/PrefectHQ/fastmcp/security/advisories/GHSA-vv7q-7jx5-f767 - Related upstream issue: https://github.com/agent0ai/agent-zero/issues/1526

Alessandro committed Apr 16, 2026 at 15:18 UTC b73da881c64dd39fb0bbb860aa6ffe1ea8d4a240
2 files changed +64 -2
requirements.txt
+2 -2
@@ -4,7 +4,7 @@ browser-use==0.5.11
4 docker==7.1.0
5 duckduckgo-search==6.1.12
6 faiss-cpu==1.11.0
7 -fastmcp==2.13.1
7 +fastmcp==3.2.4
8 fasta2a==0.5.0
9 flask[async]==3.0.3
10 flask-basicauth==0.2.0
@@ -20,7 +20,7 @@ langchain-unstructured==0.1.6
20 openai-whisper==20250625
21 lxml_html_clean>=0.4.0 # CVE-2024-52595 fix: XSS CWE-79 CVSS 8.4
22 markdown==3.7
23 -mcp==1.22.0
23 +mcp==1.27.0
24 newspaper3k==0.2.8
25 paramiko==3.5.0
26 playwright==1.52.0
tests/test_fastmcp_openapi_security.py new
+62
@@ -0,0 +1,62 @@
1 +import sys
2 +from pathlib import Path
3 +
4 +import httpx
5 +import pytest
6 +from fastmcp.server.providers.openapi import OpenAPIProvider
7 +
8 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
9 +if str(PROJECT_ROOT) not in sys.path:
10 + sys.path.insert(0, str(PROJECT_ROOT))
11 +
12 +
13 +OPENAPI_SPEC = {
14 + "openapi": "3.1.0",
15 + "info": {"title": "FastMCP security regression", "version": "1.0.0"},
16 + "paths": {
17 + "/api/v1/users/{id}/profile": {
18 + "get": {
19 + "operationId": "get_user_profile",
20 + "parameters": [
21 + {
22 + "name": "id",
23 + "in": "path",
24 + "required": True,
25 + "schema": {"type": "string"},
26 + }
27 + ],
28 + "responses": {"200": {"description": "ok"}},
29 + }
30 + }
31 + },
32 +}
33 +
34 +
35 +@pytest.mark.asyncio
36 +async def test_openapi_provider_percent_encodes_path_parameters():
37 + captured = {}
38 +
39 + async def handler(request: httpx.Request) -> httpx.Response:
40 + captured["path"] = request.url.path
41 + captured["raw_path"] = request.url.raw_path.decode("ascii")
42 + captured["authorization"] = request.headers.get("authorization")
43 + return httpx.Response(200, json={"ok": True})
44 +
45 + transport = httpx.MockTransport(handler)
46 + async with httpx.AsyncClient(
47 + base_url="http://backend.local/",
48 + headers={"Authorization": "Bearer admin_secret"},
49 + transport=transport,
50 + ) as client:
51 + provider = OpenAPIProvider(openapi_spec=OPENAPI_SPEC, client=client)
52 + tool = await provider.get_tool("get_user_profile")
53 +
54 + assert tool is not None
55 +
56 + result = await tool.run({"id": "../../../admin/delete-all?"})
57 +
58 + assert result.structured_content == {"ok": True}
59 + assert captured["authorization"] == "Bearer admin_secret"
60 + assert captured["path"].startswith("/api/v1/users/")
61 + assert captured["raw_path"].startswith("/api/v1/users/%2E%2E%2F")
62 + assert captured["raw_path"].endswith("/profile")