{"record":{"id":"29977aa572a4d5dc","repo":"HKUDS/DeepTutor","slug":"access-denied","errorCode":null,"errorMessage":"Access denied","messagePattern":"Access denied","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"deeptutor/api/routers/knowledge.py","lineNumber":345,"sourceCode":"    for raw_seg in str(rel_path).replace(\"\\\\\", \"/\").split(\"/\"):\n        seg = raw_seg.strip()\n        if seg in (\"\", \".\"):\n            continue\n        if seg == \"..\":\n            raise HTTPException(status_code=400, detail=\"Invalid folder path\")\n        safe = _sanitize_path_segment(seg)\n        if safe:\n            parts.append(safe)\n    return \"/\".join(parts)\n\n\ndef _safe_join_raw(raw_dir: Path, rel_path: str) -> Path:\n    \"\"\"Resolve ``rel_path`` under ``raw_dir``, rejecting traversal.\"\"\"\n    target = (raw_dir / rel_path).resolve()\n    try:\n        target.relative_to(raw_dir.resolve())\n    except ValueError as exc:\n        raise HTTPException(status_code=403, detail=\"Access denied\") from exc\n    return target\n\n\ndef _save_uploaded_files(\n    files: list[UploadFile],\n    target_dir: Path,\n    allowed_extensions: set[str] | None = None,\n    kb_name: str | None = None,\n    rel_paths: list[str] | None = None,\n    dest_subdir: str = \"\",\n) -> tuple[list[str], list[str]]:\n    \"\"\"\n    Save uploaded files to the local raw/ directory.\n\n    When PocketBase is enabled and ``kb_name`` is supplied, each file is also\n    uploaded to the PocketBase knowledge_bases record as a file attachment\n    (best-effort — local write is always the primary path).\n    \"\"\"","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/api/routers/knowledge.py#L327-L363","documentation":"Raised by _safe_join_raw as a second line of defense: after resolving (raw_dir / rel_path), the result does not live under raw_dir, so the resolved target escaped the KB root (symlink, traversal, or absolute path). Unlike the 400 in _sanitize_rel_subdir this returns 403 Access denied.","triggerScenarios":"create_kb_folder or move_kb_file where rel_path contains traversal that survived sanitization, or where a symlink inside raw_dir resolves outside it; passing an absolute path that resolve() anchors elsewhere.","commonSituations":"Symlinks inside the KB directory pointing to external locations; race between check and use; crafted payloads bypassing segment-level checks.","solutions":["Audit the KB raw directory for symlinks pointing outside it (find . -type l -exec readlink {} \\;)","Ensure the client only sends simple relative folder names","Re-run with the same input after removing offending symlinks; keep the segment-level sanitizer upstream of this call"],"exampleFix":"# before\nrel = '../../../../home/user/secret'\n# after\nrel = 'semester1/cs101'","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef joins_inside(root: Path, rel: str) -> bool:\n    try:\n        (root / rel).resolve().relative_to(root.resolve())\n        return True\n    except ValueError:\n        return False","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Mirror the resolve+relative_to check client-side","Keep KB directories free of symlinks escaping the root","Send only plain relative folder names"],"tags":["path-traversal","security","symlink","http-403"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}