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.