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

  1. Send a relative path that stays inside the skill directory (no '..', no leading slash, no null bytes)
  2. Resolve symlinks so requested files physically live inside the skill directory
  3. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/124ec69ca5bb1fc3. Report an issue: GitHub.