{"record":{"id":"33b236e7be69483f","repo":"unslothai/unsloth","slug":"path-is-not-in-the-browseable-allowlist-register","errorCode":null,"errorMessage":"Path is not in the browseable allowlist. Register it via POST /api/models/scan-folders first, or pick a directory under your home folder.","messagePattern":"Path is not in the browseable allowlist\\. Register it via POST /api/models/scan-folders first, or pick a directory under your home folder\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"studio/backend/routes/models.py","lineNumber":1700,"sourceCode":"                    status_code = 404,\n                    detail = f\"Path does not exist: {os.path.basename(requested_path)}\",\n                )\n            try:\n                resolved_child = child.resolve()\n            except OSError as exc:\n                logger.warning(\n                    \"browse-folders: invalid path component %r under %s: %s\",\n                    part,\n                    current,\n                    exc,\n                    exc_info = True,\n                )\n                raise HTTPException(\n                    status_code = 400,\n                    detail = \"Invalid path\",\n                ) from exc\n            if not _is_path_inside_allowlist(resolved_child, resolved_roots):\n                raise HTTPException(\n                    status_code = 403,\n                    detail = (\n                        \"Path is not in the browseable allowlist. Register it via \"\n                        \"POST /api/models/scan-folders first, or pick a directory \"\n                        \"under your home folder.\"\n                    ),\n                )\n            if contains_sensitive_path_component(str(resolved_child)):\n                raise HTTPException(\n                    status_code = 403,\n                    detail = \"Credential or configuration directories are not browseable.\",\n                )\n            if is_denied_system_path(str(resolved_child)):\n                raise HTTPException(\n                    status_code = 403,\n                    detail = \"System directories are not browseable.\",\n                )\n            current = resolved_child","sourceCodeStart":1682,"sourceCodeEnd":1718,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/models.py#L1682-L1718","documentation":"Raised as a 403 by _resolve_browse_target when a resolved path component lands outside every allowlist root — checked with _is_path_inside_allowlist(resolved_child, resolved_roots) after resolve(). Its main purpose is defeating symlink escapes: a component that exists inside a root but resolves (via symlink) to a location outside all roots is rejected before it is used.","triggerScenarios":"Browsing a path that contains a symlink pointing outside home/scan-folder roots (e.g. ~/models/pointers -> /etc); requested_path matching one root's textual prefix but resolving under a different, unlisted tree.","commonSituations":"Users organizing models with symlinks to a big external drive; linking datasets into home from system paths; moving scan folders and leaving symlinks behind.","solutions":["Register the symlink's real target as a scan folder via POST /api/models/scan-folders, then browse the real path directly.","Remove intermediate symlinks that escape the allowlist, or move/copy the data inside an allowed root.","Keep models under home/studio/outputs roots without escaping symlinks."],"exampleFix":"# before\nln -s /mnt/bigdrive/models ~/models/link\nGET /api/models/browse-folders?path=~models/link  # 403\n\n# after\nPOST /api/models/scan-folders {\"path\": \"/mnt/bigdrive/models\"}\nGET /api/models/browse-folders?path=/mnt/bigdrive/models","handlingStrategy":"validation","validationCode":"import os\n\ndef escapes_allowlist(path: str, roots: list[str]) -> bool:\n    real = os.path.realpath(path)\n    real_roots = [os.path.realpath(r) for r in roots]\n    return not any(real == r or real.startswith(r + os.sep) for r in real_roots)\n\nif escapes_allowlist(browse_path, scan_folder_roots):\n    raise ValueError('symlink target outside allowlist — register the real target instead')","typeGuard":null,"tryCatchPattern":"try:\n    entries = browse(path)\nexcept HTTPError as e:\n    if e.response.status_code == 403 and 'allowlist' in e.response.json()['detail']:\n        register_scan_folder(os.path.realpath(path))  # register real target, retry\n    else: raise","preventionTips":["Register symlink targets as scan folders instead of symlinking from inside allowed roots.","Test model-tree layout with realpath to confirm containment.","Expect this 403 on any symlink escape — it is a security guard, not a bug."],"tags":["filesystem","symlink","allowlist","security","path-traversal"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}