{"record":{"id":"96079d1dcf9c2c70","repo":"unslothai/unsloth","slug":"path-escapes-root-resolved-s-resolved-real","errorCode":null,"errorMessage":"path escapes root: {resolved!s} -> {resolved_real!s} is not under {root_real!s}","messagePattern":"path escapes root: (.+?) -> (.+?) is not under (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"studio/backend/utils/paths/storage_roots.py","lineNumber":388,"sourceCode":"\ndef _is_absolute_user_path(path: Path) -> bool:\n    expanded = str(path)\n    if os.name == \"nt\":\n        return path.is_absolute() and PureWindowsPath(expanded).is_absolute()\n    return path.is_absolute() and PurePosixPath(expanded).is_absolute()\n\n\ndef _assert_contained(resolved: Path, root: Path) -> None:\n    \"\"\"Raise ValueError if ``resolved`` realpaths outside ``root``.\"\"\"\n    try:\n        resolved_real = Path(os.path.realpath(resolved))\n        root_real = Path(os.path.realpath(root))\n    except OSError as exc:\n        raise ValueError(f\"path resolution failed: {exc}\") from exc\n    try:\n        resolved_real.relative_to(root_real)\n    except ValueError as exc:\n        raise ValueError(\n            f\"path escapes root: {resolved!s} -> {resolved_real!s} \" f\"is not under {root_real!s}\"\n        ) from exc\n\n\ndef resolve_under_root(\n    path_value: str | None,\n    *,\n    root: Path,\n    strip_prefixes: tuple[str, ...] = (),\n) -> Path:\n    \"\"\"Resolve ``path_value`` and assert the result is under ``root``.\n\n    Absolutes are accepted only if already contained (so pre-resolved\n    internal paths re-enter idempotently); schemas reject absolutes upstream.\n    \"\"\"\n    if not path_value or not str(path_value).strip():\n        return root\n","sourceCodeStart":370,"sourceCodeEnd":406,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/utils/paths/storage_roots.py#L370-L406","documentation":"Raised by _assert_contained in storage_roots.py:388-392 when the realpath of the resolved candidate does not live under the realpath of the root — the classic path-traversal containment check. Even though a relative path is joined onto root, symlinked directories inside the root can point outside, and realpath exposes that. The message shows resolved -> resolved_real vs root_real.","triggerScenarios":"resolve_under_root(\"models/foo\", root=...) where root/models is a symlink to /etc; absolute inputs that are inside string-wise but resolve elsewhere after realpath; Windows subst/junction mappings under the root; also a user-supplied absolute path outside the root passed to a resolver that accepts absolutes only when contained.","commonSituations":"Users symlink their model folder to another drive to save space and the containment check correctly blocks it; security scanners probing traversal; roots configured with symlinked children; docker volume mounts whose realpath differs from the configured root string.","solutions":["Point the resolver at the realpath of the location: configure the root to the actual target of the symlink, or move data physically under the root","If the user needs files on another volume, mount/bind that volume directly under the root rather than symlinking out","For legitimate absolute paths, use resolve_export_write_dir which intentionally passes absolutes through","Catch ValueError and surface a clear 'paths must stay under <root>' message to the user"],"exampleFix":"# before (root/models -> symlink to /data/models)\nresolve_under_root(\"models/llama.gguf\", root=Path(\"/srv/studio\"))\n# ValueError: path escapes root: /srv/studio/models/llama.gguf -> /data/models/llama.gguf ...\n\n# after: mount the volume under the root, or configure the real target\nresolve_under_root(\"llama.gguf\", root=Path(\"/data/models\").resolve())","handlingStrategy":"validation","validationCode":"def contained_under(path_str: str, root) -> bool:\n    try:\n        p = Path(os.path.realpath(Path(root) / path_str))\n        r = Path(os.path.realpath(root))\n        p.relative_to(r)\n        return True\n    except (ValueError, OSError):\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    path = resolve_under_root(value, root=root)\nexcept ValueError as exc:\n    if \"path escapes root\" in str(exc):\n        raise HTTPException(400, f\"paths must stay under {root}\") from exc\n    raise","preventionTips":["Never symlink from inside a storage root to outside locations","Configure roots by their realpath","Use resolve_export_write_dir for legitimate cross-volume writes","Treat this error as a blocked traversal attempt and log it"],"tags":["security","path-traversal","filesystem"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}