From 6b2a2d507aa54bced06132d42dd812fcef736947 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sat, 27 Jun 2026 12:10:37 -0400
Subject: [PATCH] Clarify resource path parameter safety (#4398)
---
docs/servers/resources.mdx | 32 ++++++++++++++++++++++++++++++--
docs/v2/servers/resources.mdx | 32 ++++++++++++++++++++++++++++++--
2 files changed, 60 insertions(+), 4 deletions(-)
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 77843f252..c756c5ff9 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -477,7 +477,7 @@ FastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/htm
-Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
+Resource templates support wildcard parameters that can match multiple path segments. Standard parameters (`{param}`) match a single URI segment before decoding and do not cross literal "/" boundaries in the request URI. Wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
```python {15, 23}
from fastmcp import FastMCP
@@ -522,6 +522,34 @@ Wildcard parameters are useful when:
Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
+#### Filesystem Path Safety
+
+Template parameters are decoded before your function receives them. A standard `{filename}` parameter matches one URI segment before decoding, so a request like `files://a%2Fb` passes `filename="a/b"` to the handler. Treat template values as untrusted decoded URI data whenever they determine filesystem paths.
+
+Validate the final resolved path against an allowed root before reading:
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DocsServer")
+DOCS_ROOT = Path("docs").resolve()
+
+
+@mcp.resource("docs://{filename}")
+def read_doc(filename: str) -> str:
+ requested_path = (DOCS_ROOT / filename).resolve()
+
+ if not requested_path.is_relative_to(DOCS_ROOT) or not requested_path.is_file():
+ raise ResourceError("Document not found")
+
+ return requested_path.read_text(encoding="utf-8")
+```
+
+Use wildcard parameters (`{path*}`) for resources whose URI shape intentionally includes slashes, and apply the same containment check before accessing the filesystem.
+
#### Query Parameters
@@ -716,4 +744,4 @@ The duplicate behavior options are:
-Resources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
\ No newline at end of file
+Resources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
diff --git a/docs/v2/servers/resources.mdx b/docs/v2/servers/resources.mdx
index a76734f12..f472df5ae 100644
--- a/docs/v2/servers/resources.mdx
+++ b/docs/v2/servers/resources.mdx
@@ -401,7 +401,7 @@ FastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/htm
-Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
+Resource templates support wildcard parameters that can match multiple path segments. Standard parameters (`{param}`) match a single URI segment before decoding and do not cross literal "/" boundaries in the request URI. Wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
```python {15, 23}
from fastmcp import FastMCP
@@ -446,6 +446,34 @@ Wildcard parameters are useful when:
Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
+#### Filesystem Path Safety
+
+Template parameters are decoded before your function receives them. A standard `{filename}` parameter matches one URI segment before decoding, so a request like `files://a%2Fb` passes `filename="a/b"` to the handler. Treat template values as untrusted decoded URI data whenever they determine filesystem paths.
+
+Validate the final resolved path against an allowed root before reading:
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DocsServer")
+DOCS_ROOT = Path("docs").resolve()
+
+
+@mcp.resource("docs://{filename}")
+def read_doc(filename: str) -> str:
+ requested_path = (DOCS_ROOT / filename).resolve()
+
+ if not requested_path.is_relative_to(DOCS_ROOT) or not requested_path.is_file():
+ raise ResourceError("Document not found")
+
+ return requested_path.read_text(encoding="utf-8")
+```
+
+Use wildcard parameters (`{path*}`) for resources whose URI shape intentionally includes slashes, and apply the same containment check before accessing the filesystem.
+
#### Query Parameters
@@ -634,4 +662,4 @@ The duplicate behavior options are:
- `"warn"` (default): Logs a warning, and the new resource/template replaces the old one.
- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
- `"replace"`: Silently replaces the existing resource/template with the new one.
-- `"ignore"`: Keeps the original resource/template and ignores the new registration attempt.
\ No newline at end of file
+- `"ignore"`: Keeps the original resource/template and ignores the new registration attempt.