PrefectHQ/fastmcp · error · ValueError
Invalid path: {e}
Error message
Invalid path: {e} What it means
SkillProvider.read() joins the requested file_path against the skill's base directory using safe_join, which rejects path traversal ('..'), absolute-path injection, null bytes, and symlink escapes. If safe_join raises PathEscapeError, the provider converts it to ValueError('Invalid path: ...'). This is a security boundary, not a validation nicety.
Source
Thrown at fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py:86
}
return json.dumps(manifest, indent=2)
class SkillFileTemplate(ResourceTemplate):
"""A template for accessing files within a skill."""
skill_info: SkillInfo
async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
"""Read a file from the skill directory."""
file_path = arguments.get("path", "")
# Security: reject traversal, absolute-path injection, null bytes, and
# symlink escapes before touching the filesystem.
try:
full_path = safe_join(self.skill_info.path, file_path)
except PathEscapeError as e:
raise ValueError(f"Invalid path: {e}") from e
if not full_path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
if not full_path.is_file():
raise ValueError(f"Not a file: {file_path}")
# Determine if binary or text based on mime type
mime_type, _ = mimetypes.guess_type(str(full_path))
if mime_type and mime_type.startswith("text/"):
return full_path.read_text(encoding="utf-8")
else:
return full_path.read_bytes()
async def _read(
self,
uri: str,
params: dict[str, Any],View on GitHub (pinned to 1f02114297)
Solutions
- Send a relative path that stays inside the skill directory (no '..', no leading slash, no null bytes)
- Resolve symlinks so requested files physically live inside the skill directory
- If the needed file legitimately lives elsewhere, move/copy it into the skill directory rather than linking it
Example fix
// before
await provider.read('skill://my-skill/../../etc/passwd')
// after
await provider.read('skill://my-skill/reference.md') Defensive patterns
Strategy: validation
Validate before calling
from pathlib import PurePosixPath p = PurePosixPath(user_path) assert not p.is_absolute() and '..' not in p.parts and '\x00' not in user_path, 'unsafe skill path'
Type guard
def is_safe_relpath(path: str) -> bool:
p = PurePosixPath(path)
return not p.is_absolute() and '..' not in p.parts and '\x00' not in path Try / catch
try:
content = await provider.read(uri)
except ValueError as e:
if 'Invalid path' in str(e):
raise SafePathError('path escapes skill directory') from e Prevention
- Never pass raw user/LLM strings as file_path; sanitize to a bare relative filename
- Reject absolute paths and '..' segments at your API boundary
- Avoid symlinks inside skill directories, or resolve and verify targets stay in-place
When it happens
Trigger: Calling read() (via the _read wrapper) with a file_path containing '../', a leading '/', a null byte, or a path that resolves through a symlink outside the skill directory.
Common situations: LLM- or user-supplied file paths passed straight through to the skill resource read; clients constructing URIs with encoded traversal sequences; skills whose files are symlinked to locations outside the skill folder.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Not a file: {file_path}
- Skill name {skill_name!r} would escape the target directory
- Expected integer, got {raw!r}
- Expected number, got {raw!r}
- Expected boolean, got {raw!r}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/124ec69ca5bb1fc3.
Report an issue: GitHub.