{"record":{"id":"7d50d4bf9f748ec2","repo":"odysseus-dev/odysseus","slug":"access-denied-7d50d4","errorCode":null,"errorMessage":"Access denied","messagePattern":"Access denied","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/upload_routes.py","lineNumber":166,"sourceCode":"    \"\"\"Setup upload routes with the provided handler\"\"\"\n\n    def _upload_root() -> str:\n        from src.constants import UPLOAD_DIR\n        return os.path.realpath(getattr(upload_handler, \"upload_dir\", UPLOAD_DIR))\n\n    def _path_inside_upload_dir(path: str) -> bool:\n        try:\n            return os.path.commonpath([_upload_root(), os.path.realpath(path)]) == _upload_root()\n        except Exception:\n            return False\n\n    def _resolve_upload_path(file_id: str) -> str:\n        from src.constants import UPLOAD_DIR\n        upload_root = getattr(upload_handler, \"upload_dir\", UPLOAD_DIR)\n        direct = os.path.join(upload_root, file_id)\n        if os.path.lexists(direct):\n            if not _path_inside_upload_dir(direct):\n                raise HTTPException(403, \"Access denied\")\n            if os.path.isfile(direct):\n                return direct\n            raise HTTPException(404, \"File not found\")\n\n        for root, _dirs, files in os.walk(upload_root, followlinks=False):\n            if file_id not in files:\n                continue\n            path = os.path.join(root, file_id)\n            if not _path_inside_upload_dir(path):\n                raise HTTPException(403, \"Access denied\")\n            if os.path.isfile(path):\n                return path\n            raise HTTPException(404, \"File not found\")\n\n        raise HTTPException(404, \"File not found\")\n\n    def _valid_session_id_for_owner(db, session_id: str | None, owner: str | None) -> str | None:\n        if not session_id:","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/upload_routes.py#L148-L184","documentation":"HTTP 403 from _resolve_upload_path when os.path.join(upload_root, file_id) exists (os.path.lexists is true, so symlinks count) but os.path.realpath escapes the upload root — _path_inside_upload_dir compares commonpath against _upload_root(). This is the path-traversal guard: a file_id like '../secrets.pem' or a symlink pointing outside uploads is rejected.","triggerScenarios":"Download/attach request with file_id containing traversal ('../..'), an absolute path fragment, or naming a symlink inside uploads whose target lives outside the uploads tree.","commonSituations":"Clients passing a server-side relative path or the original filename instead of the generated upload id; attackers probing with ../ sequences (the 403 is the guard working); a symlink inside uploads legitimately pointing elsewhere after a storage move.","solutions":["Only send the opaque id returned by POST /api/uploads (meta['id']), never a path or original filename","If storage moved and symlinks were used, replace them with real files or reconfigure upload_dir/UPLOAD_DIR to the new root","Sanitize file_id client-side: reject '/', '..', and backslashes before calling download endpoints"],"exampleFix":"// before\nfetch(`/api/uploads/${encodeURIComponent(filePath)}`)  // user-supplied path -> 403\n// after\nfetch(`/api/uploads/${upload.id}`)  // opaque server-generated id","handlingStrategy":"validation","validationCode":"const SAFE_ID = /^[A-Za-z0-9_-]+$/;\nif (!SAFE_ID.test(fileId) || fileId.includes('..')) throw new Error('invalid file id');","typeGuard":"function isUploadId(id: unknown): id is string {\n  return typeof id === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(id);\n}","tryCatchPattern":"if (resp.status === 403) { auditLog('path traversal blocked', fileId); neverRetrySameId(fileId); }","preventionTips":["Only use server-issued opaque ids, never paths or original filenames","Reject ids containing '/', '\\\\', or '..' before the request","Never follow user-supplied relative paths in download URLs"],"tags":["http","security","path-traversal","uploads"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}