{"record":{"id":"adcfc0bba1236c70","repo":"bytedance/deer-flow","slug":"invalid-path","errorCode":null,"errorMessage":"Invalid path","messagePattern":"Invalid path","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/app/gateway/routers/uploads.py","lineNumber":478,"sourceCode":"    \"\"\"List all files in a thread's uploads directory.\"\"\"\n    try:\n        result = await run_file_io(_list_uploaded_files_for_thread, thread_id, get_effective_user_id())\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n\n    return UploadListResponse(**result)\n\n\n@router.delete(\"/{filename}\")\n@require_permission(\"threads\", \"delete\", owner_check=True, require_existing=True)\nasync def delete_uploaded_file(thread_id: ThreadId, filename: str, request: Request) -> dict:\n    \"\"\"Delete a file from a thread's uploads directory.\"\"\"\n    try:\n        return await run_file_io(_delete_uploaded_file_for_thread, thread_id, filename, get_effective_user_id())\n    except FileNotFoundError:\n        raise HTTPException(status_code=404, detail=f\"File not found: {filename}\")\n    except PathTraversalError:\n        raise HTTPException(status_code=400, detail=\"Invalid path\")\n    except ValueError as e:\n        raise HTTPException(status_code=400, detail=str(e))\n    except Exception as e:\n        logger.error(f\"Failed to delete {filename}: {e}\")\n        raise HTTPException(status_code=500, detail=f\"Failed to delete {filename}: {str(e)}\")\n","sourceCodeStart":460,"sourceCodeEnd":484,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/uploads.py#L460-L484","documentation":"HTTP 400 raised by DELETE /threads/{thread_id}/uploads/{filename} when the filename triggers PathTraversalError — the path-safety layer rejects URL segments that would escape the thread's uploads directory (../, absolute paths, symlink tricks). This is a deliberate guard, not a bug.","triggerScenarios":"Passing a filename containing '..' segments, a leading slash, or encoded traversal sequences (e.g. %2e%2e%2f) in the DELETE path.","commonSituations":"Client concatenating unvalidated user input into the URL; penetration tests / scanners probing the endpoint; mis-encoded filenames from scripts.","solutions":["Delete by the exact plain filename returned by the uploads list endpoint — no directories, no '..', no absolute paths.","URL-encode the filename once (encodeURIComponent) when building the path.","Strip path components client-side before constructing the request."],"exampleFix":"// before\nawait fetch(`/api/threads/${tid}/uploads/${file.fullPath}`);\n\n// after\nconst name = file.fullPath.split('/').pop();\nawait fetch(`/api/threads/${tid}/uploads/${encodeURIComponent(name)}`);","handlingStrategy":"validation","validationCode":"function isSafeFilename(name) {\n  return typeof name === 'string'\n    && !name.includes('/')\n    && !name.includes('\\\\')\n    && name !== '.' && name !== '..'\n    && !name.startsWith('.');\n}\nif (!isSafeFilename(filename)) throw new Error('Invalid filename');","typeGuard":"const isSafeFilename = (n: unknown): n is string =>\n  typeof n === 'string' && /^[-. A-Za-z0-9_()]+$/.test(n) && n !== '.' && n !== '..' && !n.includes('/');","tryCatchPattern":null,"preventionTips":["Send only the basename (last path segment) of any filename.","encodeURIComponent the filename exactly once when building URLs.","Reject client-side any input containing '..', separators, or leading slashes."],"tags":["upload","http-400","path-traversal","security"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}