Skip to content

Commit 29aacce

Browse files
committed
fix: reject URL-encoded path separators in resource template parameters
ResourceTemplate.matches() URL-decodes extracted parameters but did not re-validate that the decoded values still satisfy the [^/]+ segment constraint. An attacker could send a URI like: files://..%2F..%2Fetc%2Fpasswd The encoded %2F passes the regex match on the raw URI, but after unquote() it becomes ../../etc/passwd — a path traversal payload that is then passed directly to the template function via fn(**params). The fix re-checks every decoded parameter value against the original [^/]+ pattern and returns None (no match) if any value now contains a forward slash.
1 parent 98f8ef2 commit 29aacce

File tree

1 file changed

+21
-1
lines changed

1 file changed

+21
-1
lines changed

src/mcp/server/mcpserver/resources/templates.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
from mcp.server.context import LifespanContextT, RequestT
2020
from mcp.server.mcpserver.context import Context
2121

22+
# Regex used for each URI template parameter segment.
23+
# Decoded values must still satisfy this constraint to prevent
24+
# encoded path-separator injection (e.g. ``%2F`` → ``/``).
25+
_SEGMENT_RE = re.compile(r"[^/]+")
26+
2227

2328
class ResourceTemplate(BaseModel):
2429
"""A template for dynamically creating resources."""
@@ -86,13 +91,28 @@ def matches(self, uri: str) -> dict[str, Any] | None:
8691
"""Check if URI matches template and extract parameters.
8792
8893
Extracted parameters are URL-decoded to handle percent-encoded characters.
94+
After decoding, each value is re-validated to ensure it does not contain
95+
a ``/`` character, which would indicate an encoded path separator bypass
96+
(e.g. ``%2F``). Rejecting such values prevents path-traversal attacks
97+
where an attacker could send ``..%2F..%2Fetc%2Fpasswd`` to escape the
98+
intended path segment.
8999
"""
90100
# Convert template to regex pattern
91101
pattern = self.uri_template.replace("{", "(?P<").replace("}", ">[^/]+)")
92102
match = re.match(f"^{pattern}$", uri)
93103
if match:
94104
# URL-decode all extracted parameter values
95-
return {key: unquote(value) for key, value in match.groupdict().items()}
105+
decoded = {key: unquote(value) for key, value in match.groupdict().items()}
106+
107+
# Reject any decoded value that would not have matched the
108+
# original ``[^/]+`` segment constraint. This blocks encoded
109+
# slash injection (``%2F`` → ``/``) which could allow path
110+
# traversal when the parameter is used in file-system operations.
111+
for value in decoded.values():
112+
if not _SEGMENT_RE.fullmatch(value):
113+
return None
114+
115+
return decoded
96116
return None
97117

98118
async def create_resource(

0 commit comments

Comments
 (0)