Skip to content

[COR2-475] Block path traversal in MCP tool path parameters - #15

Closed
ashiramin wants to merge 1 commit into
masterfrom
ashir/cd-321-mcp-path-traversal
Closed

ashiramin wants to merge 1 commit into
masterfrom
ashir/cd-321-mcp-path-traversal

Conversation

@ashiramin

Copy link
Copy Markdown
Contributor

Problem

FastMCP interpolates tool arguments directly into the backend URL path template, unescaped. A path-parameter value containing separators or dot-segments escapes the intended /api/v1 prefix, letting a caller reach undocumented backend routes such as /api/internal/* — with the caller's bearer token attached.

Reported by Cimpress during a penetration test (COR2-349 umbrella; this is the MCP half).

Verified on the pinned fastmcp==2.12.3:

tagOrId='my-service'            -> /api/v1/catalog/my-service/openapi
tagOrId='../../internal/users'  -> /api/internal/users/openapi        <-- escapes the prefix

Two mechanisms combine: fastmcp substitutes path params with a plain str.replace (no URL-encoding), and httpx then resolves the dot-segments per RFC 3986 when building the request — so the traversal is already resolved client-side, before anything reaches the network.

Fix

Add PathTraversalGuardMiddleware, which rejects path-parameter values containing separators or dot-segments (raw, percent-encoded, or backslash-encoded) before the request is composed. The set of guarded parameters is derived from the spec's own path templates and in: path declarations, so query and body parameters (e.g. free-text context) are unaffected.

Testing

  • tests/test_security.py — 23 tests covering the token set, param extraction, and middleware behaviour
  • Full suite: 49 passed
  • End-to-end against the real swagger.json: guard covers 17 path params; ../../internal/users is refused with no request sent, legitimate tags pass through

Notes

  • The commit message says CD-321 (the original ticket); the work is tracked under COR2-475.
  • This protects customers running the public server locally over stdio. The hosted server at mcp.cortex.io builds its own FastMCP instance and never calls create_mcp_server(), so it needs its own wiring — that is cortexapps/cortex-remote-mcp#(companion PR), which is independent of this one and does not need it to land first.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LZtC15RMphgMhtsoDh4i4u

FastMCP interpolates tool arguments into backend URL path templates. A
path-parameter value containing separators or dot-segments (raw or
percent/backslash-encoded) escaped the intended /api/v1 prefix, letting
callers probe undocumented backend routes such as /api/internal/* via the
response differential.

Add a PathTraversalGuardMiddleware that rejects such values for the
parameters interpolated into the request path (derived from the OpenAPI
path templates and in:path declarations), before any request is
forwarded. Non-path params (e.g. free-text context) are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ashiramin

Copy link
Copy Markdown
Contributor Author

Superseded by #16, which upgrades to fastmcp 4. That version percent-encodes OpenAPI path parameters, so a path-parameter value can no longer break out of its URL segment and the argument-level guard here is redundant.

Closing rather than merging to keep a single mechanism for COR2-475.

@ashiramin ashiramin closed this Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant