{"record":{"id":"124ec69ca5bb1fc3","repo":"PrefectHQ/fastmcp","slug":"invalid-path-e","errorCode":null,"errorMessage":"Invalid path: {e}","messagePattern":"Invalid path: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py","lineNumber":86,"sourceCode":"        }\n        return json.dumps(manifest, indent=2)\n\n\nclass SkillFileTemplate(ResourceTemplate):\n    \"\"\"A template for accessing files within a skill.\"\"\"\n\n    skill_info: SkillInfo\n\n    async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:\n        \"\"\"Read a file from the skill directory.\"\"\"\n        file_path = arguments.get(\"path\", \"\")\n\n        # Security: reject traversal, absolute-path injection, null bytes, and\n        # symlink escapes before touching the filesystem.\n        try:\n            full_path = safe_join(self.skill_info.path, file_path)\n        except PathEscapeError as e:\n            raise ValueError(f\"Invalid path: {e}\") from e\n\n        if not full_path.exists():\n            raise FileNotFoundError(f\"File not found: {file_path}\")\n\n        if not full_path.is_file():\n            raise ValueError(f\"Not a file: {file_path}\")\n\n        # Determine if binary or text based on mime type\n        mime_type, _ = mimetypes.guess_type(str(full_path))\n        if mime_type and mime_type.startswith(\"text/\"):\n            return full_path.read_text(encoding=\"utf-8\")\n        else:\n            return full_path.read_bytes()\n\n    async def _read(\n        self,\n        uri: str,\n        params: dict[str, Any],","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py#L68-L104","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait provider.read('skill://my-skill/../../etc/passwd')\n// after\nawait provider.read('skill://my-skill/reference.md')","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\np = PurePosixPath(user_path)\nassert not p.is_absolute() and '..' not in p.parts and '\\x00' not in user_path, 'unsafe skill path'","typeGuard":"def is_safe_relpath(path: str) -> bool:\n    p = PurePosixPath(path)\n    return not p.is_absolute() and '..' not in p.parts and '\\x00' not in path","tryCatchPattern":"try:\n    content = await provider.read(uri)\nexcept ValueError as e:\n    if 'Invalid path' in str(e):\n        raise SafePathError('path escapes skill directory') from e","preventionTips":["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"],"tags":["path-traversal","security","skill-provider","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}