mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-16 19:50:43 +00:00
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
62 lines
2 KiB
Python
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")
|