# REPRO-2026-00138: FastMCP: path traversal to authenticated SSRF in OpenAPIProvider _build_url() ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00138 GHSA: GHSA-vv7q-7jx5-f767 CVE: CVE-2026-32871 ## Package Name: fastmcp Ecosystem: pip Affected: < 3.2.0 Fixed: 3.2.0 ## Root Cause # RCA Report — CVE-2026-32871 ## Summary FastMCP's `OpenAPIProvider` exposes backend REST APIs as MCP tools. The `RequestDirector._build_url()` method substitutes client-supplied path parameters into the URL template by simple string replacement (`str(param_value)`) without URL-encoding. When a client passes traversal sequences such as `../../../admin`, `urllib.parse.urljoin` resolves the resulting relative path against the base URL, causing the request to escape the intended API prefix and hit arbitrary endpoints on the same backend host — an authenticated SSRF because the provider's configured `Authorization` headers travel with the request. ## Impact - **Package**: `fastmcp` (PyPI) - **Component**: `fastmcp.utilities.openapi.director.RequestDirector._build_url()` - **Affected versions**: `< 3.2.0` (tested on `3.1.1` as the latest pre-fix release) - **Fixed version**: `3.2.0` - **Risk**: High — any MCP client that can supply arguments to an OpenAPI-backed tool can redirect the outgoing HTTP request to internal endpoints, exfiltrating data or triggering unauthorized actions with the provider's own credentials. ## Root Cause In `fastmcp < 3.2.0` the `_build_url()` implementation was: ```python 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)) return urljoin(base_url.rstrip("/") + "/", url_path.lstrip("/")) ``` Because `str(param_value)` is inserted verbatim, a payload of `../../../admin` in a template `/users/{id}/profile` yields a path string `users/../../../admin/profile`. `urljoin` then normalizes this relative to the base URL `http://host/api/v1/` and resolves it to `http://host/admin/profile`, completely escaping the `/api/v1/` prefix. The fix in `3.2.0` replaces raw substitution with: ```python safe_value = quote(str(param_value), safe="").replace(".", "%2E") url_path = url_path.replace(placeholder, safe_value) ``` `quote` encodes `/` to `%2F` and `.` to `%2E`, so traversal sequences become literal strings inside the path segment (e.g., `%2E%2E%2F%2E%2E%2F%2E%2E%2Fadmin`) and `urljoin` no longer interprets them as directory traversal. ## Reproduction Steps The reproduction script is `repro/reproduction_steps.sh`. It performs the following: 1. Creates two isolated virtualenvs and installs `fastmcp==3.1.1` (vulnerable) and `fastmcp==3.2.0` (fixed). 2. For each version, instantiates `OpenAPIProvider` with an OpenAPI spec that exposes only `/users/{id}/profile`. 3. Retrieves the generated `OpenAPITool` named `get_user_profile`. 4. Executes `tool.run({'id': '../../../admin'})` against a custom `httpx.AsyncClient` using a recording transport so no real network is needed. 5. Captures the final request URL and the HTTP path actually sent. 6. Compares the vulnerable and fixed behavior and exits `0` when the vulnerability is confirmed. ### Expected evidence | Build | Request path sent by `OpenAPITool` | |-------|------------------------------------| | `3.1.1` (vulnerable) | `/admin/profile` — escaped the `/api/v1/` prefix | | `3.2.0` (fixed) | `/api/v1/users/../../../admin/profile` — traversal sequences are percent-encoded and remain inside the intended path | ## Evidence Captured artifacts are stored under `logs/`: - `logs/vuln_result.json` — JSON record for the vulnerable run showing `escaped_api_prefix: true` and `reached_unexposed_endpoint: true`. - `logs/fixed_result.json` — JSON record for the fixed run showing `escaped_api_prefix: false` and `reached_unexposed_endpoint: false`. - `logs/runtime_manifest.json` — consolidated manifest with versions, URLs, and verdict flags. Key excerpts from the vulnerable run (`logs/vuln_result.json`): ```json { "version": "3.1.1", "url": "http://127.0.0.1:9876/admin/profile", "path": "/admin/profile", "escaped_api_prefix": true, "reached_unexposed_endpoint": true } ``` Key excerpts from the fixed run (`logs/fixed_result.json`): ```json { "version": "3.2.0", "url": "http://127.0.0.1:9876/api/v1/users/%2E%2E%2F%2E%2E%2F%2E%2E%2Fadmin/profile", "path": "/api/v1/users/../../../admin/profile", "escaped_api_prefix": false, "reached_unexposed_endpoint": false } ``` Environment details: - Python 3.11 - `fastmcp==3.1.1` (vulnerable) - `fastmcp==3.2.0` (fixed) - `httpx` (for the recording transport) ## Recommendations / Next Steps 1. **Upgrade immediately** to `fastmcp >= 3.2.0`. 2. **Validate input encoding** — any code that builds URLs from user input should percent-encode path segments and encode `.` characters to prevent `..` normalization. 3. **Regression tests** — the existing `TestPathTraversalPrevention` test class in `tests/utilities/openapi/test_director.py` already covers this; ensure it remains part of the CI pipeline. 4. **Defense in depth** — consider adding server-side URL path normalization checks or allow-listing in the reverse proxy / API gateway in front of the backend. ## Additional Notes - **Idempotency**: `repro/reproduction_steps.sh` was executed twice consecutively and produced identical verdicts both times. - **Edge cases tested**: The same payload `../../../admin` was used for both runs. The fixed version correctly encodes both slashes and dots, so double-encoded payloads (`..%2F..%2Fadmin`) are also handled safely because they are encoded a second time. - **Limitations**: The reproduction uses a synthetic `httpx` recording transport rather than a live MCP client/server handshake. This is sufficient because the bug lives entirely in the request-building layer (`RequestDirector._build_url() → OpenAPITool.run()`), which the script exercises end-to-end through the actual fastmcp code path. ## Reproduction Details Reproduced: 2026-05-22T09:52:12.592Z Duration: 2236 seconds Tool calls: 204 Turns: 171 Handoffs: 3 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00138 pruva-verify GHSA-vv7q-7jx5-f767 pruva-verify CVE-2026-32871 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00138&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00138/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-vv7q-7jx5-f767 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-32871 ## Artifacts - repro/rca_report.md (analysis, 5776 bytes) - repro/reproduction_steps.sh (reproduction_script, 6333 bytes) - vuln_variant/rca_report.md (analysis, 7697 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 11336 bytes) - bundle/context.json (other, 3054 bytes) - bundle/metadata.json (other, 564 bytes) - bundle/ticket.md (ticket, 3471 bytes) - repro/validation_verdict.json (other, 783 bytes) - vuln_variant/root_cause_equivalence.json (other, 1545 bytes) - vuln_variant/patch_analysis.md (documentation, 4719 bytes) - vuln_variant/variant_manifest.json (other, 2968 bytes) - vuln_variant/validation_verdict.json (other, 2944 bytes) - logs/attempt2_fixed_tool_path.log (log, 96 bytes) - logs/fixed_result.log (log, 265 bytes) - logs/vuln_result.json (other, 188 bytes) - logs/attempt3_encoded_tool_path.log (log, 250 bytes) - logs/fixed_result.json (other, 253 bytes) - logs/runtime_manifest.json (other, 340 bytes) - logs/attempt4_resource_read_path.log (log, 345 bytes) - logs/vuln_result.log (log, 145 bytes) - logs/attempt5_proxy_resource_path.log (log, 299 bytes) - logs/backend_requests.log (log, 70 bytes) - logs/attempt1_vuln_tool_path.log (log, 59 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00138 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00138/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00138 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev