mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Screen templated resource parameters for path traversal by default (#4482)
* Add ResourceSecurity screening for templated resources (defaults on)
* Add tests for resource path-security screening
* Document resource path-security; fix ty in tests
* Carry child template security policy through provider mount
Preserve a mounted template's explicit ResourceSecurity (per-param
exemptions or a deliberate opt-out) through FastMCPProviderResourceTemplate.wrap
so the parent read chokepoint honours it instead of the parent default.
* Defer mcp SDK import so fastmcp.resources loads without the [mcp] extra
* Make resource path-security docs examples self-contained and runnable
* Match exempt_params under both hyphen and underscore spellings
Template placeholders like {git-ref} extract as git_ref, so an exemption
written with the natural URI-template spelling never matched.
* Docs: describe net-depth traversal rule accurately; make example runnable
The screening only rejects .. segments that escape the starting depth
(foo/../bar passes) — saying any standalone .. is rejected overstated
the guarantee. Also define DOCS_ROOT so the example runs.
This commit is contained in:
parent
918b85f9b2
commit
d779414f8a
12 changed files with 824 additions and 11 deletions
|
|
@ -324,6 +324,14 @@ FastMCP keeps its own DCR redirect-URI hardening (PRs #4419, #4408) regardless o
|
|||
|
||||
*Verify:* recent commits `67527c1f` (block unsafe OAuth redirect schemes), `57a27992` (DNS rebinding), `cccb529f` (DCR redirect URI validation) on `main`.
|
||||
|
||||
### Templated resource parameters are path-screened by default — Breaking (behavior)
|
||||
|
||||
Every templated resource now has its extracted parameter values screened for path-traversal (`..` segments), absolute paths, and null bytes **before the handler runs** — on by default, at the server's read chokepoint, covering local and provider-sourced (mounted/proxied) templates alike. Previously these payloads reached handlers raw; a template whose parameter flowed into a filesystem path or upstream URL was exposed unless the author added their own check. A rejected read now surfaces a non-leaky "resource not found" error (`-32602`) and a debug log.
|
||||
|
||||
The check is component-based, matching the SDK's `contains_path_traversal`: only a standalone `..` segment is traversal, so values that merely contain dots (`HEAD~3..HEAD`, `file.tar.gz`) and dotfiles (`.env`) still pass. This can break a template that legitimately accepts `..`-bearing or absolute values — exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/resources/security.py` (`ResourceSecurity`), the screening block in `FastMCP.read_resource` (`fastmcp_slim/fastmcp/server/server.py`), and `tests/resources/test_resource_security.py`.
|
||||
|
||||
## Removed in 4.0
|
||||
|
||||
Deprecations that warned in 3.x are removed in 4.0. Each entry below is a hard removal — the old surface raises `TypeError` / `AttributeError` rather than warning, unless noted otherwise.
|
||||
|
|
|
|||
|
|
@ -522,11 +522,85 @@ 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
|
||||
#### Path Security
|
||||
|
||||
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.
|
||||
Template parameters are extracted from the request URI and decoded before your function receives them, so a path-traversal payload like `../` or an absolute path can reach a handler that builds filesystem paths or upstream URLs. FastMCP screens every templated resource's parameter values **before the handler runs**, and this screening is **on by default**.
|
||||
|
||||
Validate the final resolved path against an allowed root before reading:
|
||||
By default, a parameter value is rejected if its `..` path segments would escape the value's own starting depth, if it looks like an absolute path, or if it contains a null byte. A rejected read surfaces a clean "resource not found" error to the client and logs the reason at debug level, so the failing parameter and policy are never revealed on the wire.
|
||||
|
||||
The traversal check is component-based and tracks net depth: `..` only counts against you when it climbs above where the value starts. `../secret`, a bare `..`, and `a/../../b` are rejected; `foo/../bar` is allowed because it never leaves the starting directory, and values that merely *contain* dots — `HEAD~3..HEAD`, `v1..v2`, `file.tar.gz`, dotfiles like `.env` — all pass. Screening runs on the decoded value, so `..%2F` is caught the same as a literal `../`. This bounds relative escapes; anchoring the *final* path inside a root directory is still your handler's job (for example with `safe_join`), since only the handler knows what the value is joined to.
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="DocsServer")
|
||||
|
||||
DOCS_ROOT = Path("/srv/docs")
|
||||
|
||||
|
||||
@mcp.resource("docs://{path*}")
|
||||
def read_doc(path: str) -> str:
|
||||
# A request for docs://../secret is rejected before this runs.
|
||||
return (DOCS_ROOT / path).read_text(encoding="utf-8")
|
||||
```
|
||||
|
||||
##### Exempting parameters
|
||||
|
||||
Some parameters legitimately carry values that look like traversal — a git ref, a version range, an opaque token. Exempt them by name with `ResourceSecurity`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.resources import ResourceSecurity
|
||||
|
||||
mcp = FastMCP(name="DocsServer")
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"git://diff/{ref}",
|
||||
security=ResourceSecurity(exempt_params={"ref"}),
|
||||
)
|
||||
def git_diff(ref: str) -> str:
|
||||
# ref="HEAD~3..HEAD" is allowed
|
||||
...
|
||||
```
|
||||
|
||||
##### Disabling screening
|
||||
|
||||
Pass `security=None` to turn screening off for a single component:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(name="DocsServer")
|
||||
|
||||
|
||||
@mcp.resource("raw://{value}", security=None)
|
||||
def raw(value: str) -> str: ...
|
||||
```
|
||||
|
||||
Or set a server-wide default with `resource_security`, which applies to every templated resource that does not set its own `security`:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.resources import ResourceSecurity
|
||||
|
||||
# Relax one check across the whole server:
|
||||
relaxed = FastMCP(
|
||||
name="DocsServer",
|
||||
resource_security=ResourceSecurity(reject_absolute_paths=False),
|
||||
)
|
||||
|
||||
# Or disable screening entirely across the server:
|
||||
unscreened = FastMCP(name="DocsServer", resource_security=None)
|
||||
```
|
||||
|
||||
A per-component `security` always overrides the server default.
|
||||
|
||||
<Warning>
|
||||
Screening rejects the obvious injection shapes, but it does not know your filesystem root. When a parameter determines a real path, still resolve it against an allowed root and confirm containment before reading — screening and containment are complementary layers.
|
||||
</Warning>
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
|
@ -548,8 +622,6 @@ def read_doc(filename: str) -> str:
|
|||
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
|
||||
|
||||
<VersionBadge version="2.13.0" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue