**SQLBot Version** 最新 `main`(核实于 f10255b) **Run Mode** 源码运行;接口为 `POST /system/download-fail-info`,不需要数据库即可复现。 **Describe the bug** `backend/apps/settings/api/base.py` 直接把请求体里的 `file` 拼到 `settings.EXCEL_PATH` 上,先判断存在性、后判断后缀,且没有任何路径归属校验: ```python filename = req.file file_path = os.path.join(path, filename) if not os.path.exists(file_path): raise HTTPException(404, "File Not Exists") if not filename.endswith('_error.xlsx'): raise HTTPException(400, "Only support _error.xlsx") filename = os.path.basename(file_path) return FileResponse(path=file_path, filename=filename, media_type=...) ``` 由此有两个问题: 1. **可以读取上传目录之外的文件**:只要名字以 `_error.xlsx` 结尾,`../`、绝对路径等取值都会被直接打开并返回(POSIX 主机即可复现;Windows 主机上 `..\..\` 与 `C:` 驱动器相对路径同样有效); 2. **存在性探测**:存在性检查排在后缀检查之前,因此"路径存在但后缀不合规"返回 400,"路径不存在"返回 404 —— 仅凭状态码就能判断任意路径是否存在,无需通过后缀校验。 **To Reproduce** 1. 准备上传目录内的 `<EXCEL_PATH>/orders_error.xlsx`,以及目录外的 `<tmp>/outside/secret_error.xlsx`; 2. `POST /system/download-fail-info`,body 为 `{"file": "../outside/secret_error.xlsx"}`:修复前返回 200 与目录外文件内容; 3. `{"file": "missing.xlsx"}` → 404,`{"file": "orders.xlsx"}`(文件存在但后缀不合规)→ 400,两者可区分,可用于探测存在性。 本地实际执行(真实路由挂载到临时应用、上传目录重定向到临时目录):`../outside/secret_error.xlsx` 与 `./../outside/secret_error.xlsx` 都返回 200 与目录外内容;`orders.xlsx` / `missing.xlsx` 分别返回 400 / 404;`sub_error.xlsx`(同名目录)返回 200 而不是拒绝。 **Expected behavior** - `file` 只接受导出接口生成的单个文件名:带目录分量、盘符或控制字符的取值直接拒绝,不做静默改写; - 先完成文件名与后缀校验,再访问文件系统;随后用 `os.path.realpath` + `os.path.commonpath` 确认解析结果仍在上传目录内(符号链接一并解析),并且只返回普通文件; - 拒绝路径不回显内部绝对路径或暴露存在性。 **Additional context** - 生成该文件名的导出接口(`apps/terminology/api/terminology.py`、`apps/data_training/api/data_training.py`)返回的都是纯文件名,因此收紧取值不会影响正常导出流程; - 同类问题在写入侧已修复(#1448 / #1449),读取侧可以复用同一个 `common.utils.utils.safe_upload_name` 保持口径一致。
Activity
fix(system): validate the fail-info download name and path #1454