{"record":{"id":"d2ecf44fa8bf1490","repo":"unslothai/unsloth","slug":"directory-not-allowed-d2ecf4","errorCode":null,"errorMessage":"Directory not allowed","messagePattern":"Directory not allowed","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"studio/backend/routes/models.py","lineNumber":1172,"sourceCode":"    if _safe_is_dir(legacy_hf):\n        allowed_roots.append(legacy_hf)\n    if _safe_is_dir(hf_default):\n        allowed_roots.append(hf_default)\n    try:\n        from utils.paths import studio_root, outputs_root\n        allowed_roots.extend([studio_root(), outputs_root()])\n    except Exception:\n        pass\n\n    requested = os.path.realpath(os.path.expanduser(models_dir))\n    models_root = None\n    for root in allowed_roots:\n        root_str = os.path.realpath(str(root))\n        if requested == root_str or requested.startswith(root_str + os.sep):\n            models_root = root  # trusted root, not the user-supplied path\n            break\n    if models_root is None:\n        raise HTTPException(\n            status_code = 403,\n            detail = \"Directory not allowed\",\n        )\n\n    try:\n        models = await _shared_compat_local_inventory_scan(models_root, sources)\n        # Tag each model with its task so the Images picker can filter to diffusion.\n        models = [m.model_copy(update = {\"task\": _local_model_task(m)}) for m in models]\n\n        return LocalModelListResponse(\n            models_dir = str(models_root),\n            hf_cache_dir = str(hf_cache_dir),\n            lmstudio_dirs = [str(d) for d in lm_dirs],\n            models = models,\n        )\n    except Exception as e:\n        raise log_and_http_error(\n            e,","sourceCodeStart":1154,"sourceCodeEnd":1190,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/models.py#L1154-L1190","documentation":"Raised as a 403 by the local-models listing endpoint when the requested models_dir, after expanduser + realpath, is neither equal to nor a subdirectory of any allowed root (home directory, studio root, outputs root, registered scan folders). The check uses resolved paths on both sides, so symlink tricks do not bypass it; the trusted allowlist root is used for the scan, never the user-supplied path.","triggerScenarios":"GET /api/models/local?models_dir=/etc or any path outside the allowlist; a path containing '..' that resolves outside a root; a symlink whose realpath target lies outside all allowed roots; a directory that was never registered via POST /api/models/scan-folders.","commonSituations":"Front-end remembers a models directory from another machine/user; user pastes /var/data/models or a mount point outside home; models stored on an external drive not registered as a scan folder; case/format differences in the path that make the prefix check fail.","solutions":["Register the directory first via POST /api/models/scan-folders with the absolute path, then retry the models listing.","Point models_dir at a directory under your home folder, the studio root, or the outputs root.","Verify the realpath: the comparison uses os.path.realpath on both sides, so confirm the resolved location (e.g. readlink -f /your/path) is actually inside an allowed root.","Check for symlinks in the path whose targets escape the allowlist and register the real target instead."],"exampleFix":"# before\nresp = client.get('/api/models/local', params={'models_dir': '/mnt/bigdrive/models'})  # 403\n\n# after\nclient.post('/api/models/scan-folders', json={'path': '/mnt/bigdrive/models'})\nresp = client.get('/api/models/local', params={'models_dir': '/mnt/bigdrive/models'})","handlingStrategy":"validation","validationCode":"import os\n\ndef allowed(models_dir: str, scan_folders: list[str]) -> bool:\n    req = os.path.realpath(os.path.expanduser(models_dir))\n    roots = [os.path.realpath(os.path.expanduser('~'))] + [os.path.realpath(f) for f in scan_folders]\n    return any(req == r or req.startswith(r + os.sep) for r in roots)","typeGuard":null,"tryCatchPattern":"try:\n    resp = client.get('/api/models/local', params={'models_dir': d})\nexcept HTTPError as e:\n    if e.response.status_code == 403:\n        register_scan_folder(d); resp = client.get('/api/models/local', params={'models_dir': d})\n    else: raise","preventionTips":["Always register non-home model directories via POST /api/models/scan-folders before listing.","Send absolute paths; let the server do ~ expansion consistently.","Remember the check is on realpath — avoid symlinks that resolve outside allowed roots."],"tags":["models","filesystem","allowlist","security","path-traversal"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}