Title: fix: reject URL-encoded path separators in resource template parameters by sebastiondev · Pull Request #2353 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: fix: reject URL-encoded path separators in resource template parameters by sebastiondev · Pull Request #2353 · modelcontextprotocol/python-sdk
X Title: fix: reject URL-encoded path separators in resource template parameters by sebastiondev · Pull Request #2353 · modelcontextprotocol/python-sdk
Description: Vulnerability Summary CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal") Severity: High Affected file: src/mcp/server/mcpserver/resources/templates.py — ResourceTemplate.matches() Data Flow A client sends an MCP resources/read request with a crafted URI (e.g. file://documents/../../../etc/passwd). MCPServer._handle_read_resource() routes the request to ResourceManager.get_resource() (line 94, resource_manager.py). ResourceTemplate.matches() converts the URI template to a regex where each {param} becomes (?P[^/]+). The regex matches the encoded URI — ../../../etc/passwd contains no literal /, so [^/]+ succeeds. unquote() then decodes it to ../../../etc/passwd, which is returned as the parameter value. If the server-side resource handler uses this value in file-system operations (a common pattern shown in official examples), path traversal occurs. Exploit Sketch # Server code (common pattern from official examples) @mcp.resource("file://documents/{name}") def read_document(name: str) -> str: return Path(f"/data/documents/{name}").read_text() // Attack payload {"method": "resources/read", "params": {"uri": "file://documents/../../../secrets/api_keys.json"}} Before fix: name = "../../../secrets/api_keys.json" → resolves to /secrets/api_keys.json After fix: matches() returns None → request is rejected Fix Description The fix adds a post-decode validation step in ResourceTemplate.matches(). After URL-decoding extracted parameter values, each value is re-validated against the same [^/]+ segment constraint that was applied to the encoded form. If a decoded value contains a / character (which could only come from an encoded /), matches() returns None. Rationale Single chokepoint: matches() is the only code path that extracts template parameters from URIs in the MCP server framework. Fixing it here provides defense-in-depth for all resource handlers. Minimal and targeted: 1 file changed, 21 insertions, 1 deletion. No behavioral change for legitimate URIs (which never contain encoded path separators in parameter segments). Consistent with RFC 3986 §2.2: / is a reserved character; / in a path segment should not be treated as equivalent to a literal / for matching purposes. Prior art: This is the same class of vulnerability as CVE-2021-41773 (Apache httpd path traversal via . encoding) and similar issues fixed in Flask/Werkzeug. Change Details # New module-level constant _SEGMENT_RE = re.compile(r"[^/]+") # In matches(), after decoding: decoded = {key: unquote(value) for key, value in match.groupdict().items()} for value in decoded.values(): if not _SEGMENT_RE.fullmatch(value): return None return decoded Test Results Summary Tested the following scenarios to confirm correctness: Test Case Input URI Expected Result Normal match file://docs/readme.txt {"name": "readme.txt"} ✅ Pass Encoded slash / file://docs/../../etc/passwd None (rejected) ✅ Pass Encoded slash / (lowercase) file://docs/../../etc/passwd None (rejected) ✅ Pass Double-encoded %2F file://docs/..%2F..%2Fetc {"name": "../../etc"} (single decode, no /) ✅ Pass Legitimate encoded chars file://docs/hello world.txt {"name": "hello world.txt"} ✅ Pass Multiple parameters file://{org}/{repo} with / in one param None (rejected) ✅ Pass Empty-after-decode file://docs// None (rejected) ✅ Pass Non-matching URI file://other/path against file://docs/{name} None ✅ Pass (no regression) Disprove Analysis We systematically attempted to disprove this finding across multiple dimensions: Authentication Check No authentication on ResourceTemplate.matches() or read_resource. Auth is opt-in via MCPServer(auth=...). Default servers run without auth. The resources/read handler is accessible to any connected MCP client. Network Check Default transport is stdio (local process). HTTP transports default to 127.0.0.1 with DNS rebinding protection. However, servers can be configured to bind to 0.0.0.0 or deployed over HTTP — the README shows streamable HTTP examples suggesting production HTTP use is expected. Caller Trace matches() is called from exactly one place: ResourceManager.get_resource() → MCPServer.read_resource() → _handle_read_resource(). The URI comes directly from params.uri (client-supplied). Attacker-controlled data reaches this code path. Prior Validation Zero validation existed on decoded parameter values before this fix. Pydantic validate_call only enforces type (str), not content. Existing Mitigations Default stdio transport reduces remote attack surface HTTP transport defaults to localhost with DNS rebinding protection OAuth auth is available but opt-in No framework-level path sanitization beyond this fix Similar Code Paths The low-level server API does not use ResourceTemplate — it delegates to user callbacks directly. This is not a parallel vulnerability because low-level users don't use templates and are responsible for their own parsing. No other instances of template parameter extraction without post-decode validation exist. Known Limitation The fix does not block \ → \ (Windows backslash path separator). On Windows, ..\..\secret is a valid traversal. This is a separate concern given the URI spec uses / exclusively, and could be addressed in a follow-up if desired. Verdict Confirmed valid with high confidence. The vulnerability is clearly demonstrable, the fix is correct and minimal, and it addresses the primary attack vector at the single chokepoint for template parameter extraction. Preconditions for Exploitation MCP client access: Attacker must send MCP protocol messages (via compromised/prompt-injected LLM, direct HTTP connection, or local stdio) Vulnerable resource handler: Server must use extracted template parameters in file-system operations without additional validation No additional path sanitization in user handler code This fix was prepared through a 4-stage review process including vulnerability identification, fix development, testing, and adversarial disprove analysis.
Open Graph Description: Vulnerability Summary CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal") Severity: High Affected file: src/mcp/server/mcpserver/resources/templates.py ...
X Description: Vulnerability Summary CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal") Severity: High Affected file: src/mcp/server/mcpserver/resources/templ...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2353
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:91edcbb7-ef36-cde0-37f9-6bf4074d50d9 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | A14C:23C361:19FF192:24E024C:6A58FFEF |
| html-safe-nonce | 82392575b4bf3f1fa4e15a4fdf55388cca2e6e320ecaba6c645837c3b9cc9f3f |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBMTRDOjIzQzM2MToxOUZGMTkyOjI0RTAyNEM6NkE1OEZGRUYiLCJ2aXNpdG9yX2lkIjoiMjQ2OTc4NjU2NDEzNTgxMzEwMyIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | fb21eec067ee1d7cd602025a536bbf44141394a2d265795c3a0253cea0c1779d |
| hovercard-subject-tag | pull_request:3450869360 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2353/files |
| twitter:image | https://avatars.githubusercontent.com/u/262984339?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/262984339?s=400&v=4 |
| og:image:alt | Vulnerability Summary CWE-22: Improper Limitation of a Pathname to a Restricted Directory ("Path Traversal") Severity: High Affected file: src/mcp/server/mcpserver/resources/templates.py ... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5f2a0c7865178af3d91dd9f13b0cdfc3c73a2529c873d2780bb4c01271a57ec6 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9842457e8d7e3b2e1635a63af28da35614845510 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width