agent-zero/tests/test_fastmcp_openapi_security.py
Alessandro b73da881c6 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
2026-04-16 15:21:01 +02:00

62 lines
2 KiB
Python

import sys
from pathlib import Path
import httpx
import pytest
from fastmcp.server.providers.openapi import OpenAPIProvider
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
OPENAPI_SPEC = {
"openapi": "3.1.0",
"info": {"title": "FastMCP security regression", "version": "1.0.0"},
"paths": {
"/api/v1/users/{id}/profile": {
"get": {
"operationId": "get_user_profile",
"parameters": [
{
"name": "id",
"in": "path",
"required": True,
"schema": {"type": "string"},
}
],
"responses": {"200": {"description": "ok"}},
}
}
},
}
@pytest.mark.asyncio
async def test_openapi_provider_percent_encodes_path_parameters():
captured = {}
async def handler(request: httpx.Request) -> httpx.Response:
captured["path"] = request.url.path
captured["raw_path"] = request.url.raw_path.decode("ascii")
captured["authorization"] = request.headers.get("authorization")
return httpx.Response(200, json={"ok": True})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(
base_url="http://backend.local/",
headers={"Authorization": "Bearer admin_secret"},
transport=transport,
) as client:
provider = OpenAPIProvider(openapi_spec=OPENAPI_SPEC, client=client)
tool = await provider.get_tool("get_user_profile")
assert tool is not None
result = await tool.run({"id": "../../../admin/delete-all?"})
assert result.structured_content == {"ok": True}
assert captured["authorization"] == "Bearer admin_secret"
assert captured["path"].startswith("/api/v1/users/")
assert captured["raw_path"].startswith("/api/v1/users/%2E%2E%2F")
assert captured["raw_path"].endswith("/profile")