{"record":{"id":"8d7b2b93b6cbbcd6","repo":"can1357/oh-my-pi","slug":"unsafe-scheme-path-absolute-or-traversal","errorCode":null,"errorMessage":"Unsafe {scheme}:// path (absolute or traversal): {path}","messagePattern":"Unsafe (.+?):// path \\(absolute or traversal\\): (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/eval/py/prelude.py","lineNumber":112,"sourceCode":"            return Path(path)\n        scheme = match.group(1).lower()\n        try:\n            roots = json.loads(os.environ.get(\"PI_EVAL_LOCAL_ROOTS\") or \"{}\")\n        except (ValueError, TypeError):\n            roots = {}\n        root = roots.get(scheme) if isinstance(roots, dict) else None\n        if not root:\n            raise ValueError(f\"Protocol paths are not supported by this helper: {path}\")\n        relative = unquote(match.group(2).replace(\"\\\\\", \"/\"))\n        # Mirror the host `path.resolve`/`resolveLocalUrlToPath`: normalize and\n        # make absolute WITHOUT realpath'ing symlinks (Path.resolve would turn\n        # /tmp into /private/tmp and diverge from the read-side resolution).\n        root_path = os.path.abspath(root)\n        if relative == \"\":\n            return Path(root_path)\n        rel_path = Path(relative)\n        if rel_path.is_absolute() or \"..\" in rel_path.parts:\n            raise ValueError(f\"Unsafe {scheme}:// path (absolute or traversal): {path}\")\n        resolved = os.path.abspath(os.path.join(root_path, relative))\n        if resolved != root_path and not resolved.startswith(root_path + os.sep):\n            raise ValueError(f\"{scheme}:// path escapes its root: {path}\")\n        return Path(resolved)\n\n    def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:\n        \"\"\"Read file or read-tool URI contents. offset/limit are 1-indexed lines.\"\"\"\n        if _should_delegate_read(path):\n            if limit is not None and limit <= 0:\n                return \"\"\n            selector = _read_line_selector(offset, limit)\n            tool_path = path if selector is None else f\"{path}:{selector}\"\n            return _read_tool_text(tool_path)\n        p = _resolve_omp_path(path)\n        data = p.read_text(encoding=\"utf-8\")\n        lines = data.splitlines(keepends=True)\n        if offset > 1 or limit is not None:\n            start = max(0, offset - 1)","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/eval/py/prelude.py#L94-L130","documentation":"_resolve_omp_path rejects scheme paths whose relative component is absolute (e.g. scheme:///abs/x) or contains '..' segments, raising ValueError 'Unsafe <scheme>:// path (absolute or traversal)'. This is a path-traversal guard: every resolved path must stay inside the configured root for that scheme.","triggerScenarios":"read()/write() with a URI whose path after the scheme is absolute ('scheme:///etc/passwd') or climbs out with '..' ('scheme://../../secret').","commonSituations":"LLM-generated code constructing URIs with leading slashes or trying to reach files outside the session root; joining user-supplied relative paths that include '..'; echoing back absolute paths captured from host tool output into a scheme URI.","solutions":["Rewrite the URI to a path relative to the scheme root with no leading '/' and no '..' segments.","Copy the target file into the root directory configured for that scheme, then reference it relatively.","If the file genuinely lives outside the root, access it via a plain local path if the harness permits, or extend PI_EVAL_LOCAL_ROOTS to mount the needed directory."],"exampleFix":"# before\nread(\"omp:///../outside/secret.txt\")\n# after\nread(\"omp://artifacts/result.json\")  # relative, no traversal","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\ndef is_safe_rel(rel): p = PurePosixPath(rel); return not p.is_absolute() and '..' not in p.parts","typeGuard":"def is_protocol_path(p): return '://' in str(p)","tryCatchPattern":"try:\n    data = read(uri)\nexcept ValueError as e:\n    if 'Unsafe' in str(e) or 'traversal' in str(e):\n        raise PermissionError(f'Refusing path outside session root: {uri}') from e\n    raise","preventionTips":["Normalize any user/model-supplied URI: strip leading slashes and resolve '..' against the intended target.","Treat 'Unsafe ... path' errors as inputs to sanitize, not paths to work around.","Copy external files into the scheme root host-side instead of traversing to them.","Reject '..' segments at input-validation time in generated code review."],"tags":["python","path-traversal","security","prelude"],"backgroundTag":"path-traversal-blocked","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}