12 KiB
安全加固总结(前端 / 后端 / 算力节点)
记录 2026-08-06 对本平台的漏洞修复。核心目标:修复 FastAPI 文档接口未授权访问、 Swagger 泄露 API 结构、以及两类任意文件读取漏洞(路径穿越 + 符号链接跟随), 修复过程不改变正常业务流程。
其中 FastAPI 文档开关(
ENABLE_DOCS) 的详细用法见 §4 FastAPI 文档开关使用说明。
1. 漏洞总览
| # | 影响面 | 漏洞 | 风险等级 | 修复 |
|---|---|---|---|---|
| 1 | 后端 + 算力节点 | FastAPI 默认暴露 /docs、/redoc、/openapi.json,未授权泄露全部 API 结构、参数、内部路由 |
中 | 生产环境关闭文档路由,访问返回 404(ENABLE_DOCS 可覆盖) |
| 2 | 后端 | data-convert 模块 output_filename 路径穿越:可任意文件读 / 写 / 删,且整个模块无鉴权 |
严重 | 输出文件名白名单校验 + 全部端点补鉴权 |
| 3 | 算力节点 | compute/files/{file_id}/download:file_id 直接拼进 glob 模式可 ../ 穿越出上传目录,FileResponse 在 Linux 上跟随符号链接读取任意文件 |
中高 | file_id 字符白名单 + 解析后路径包含性二次校验 |
| 4 | 前端(Vue + nginx) | 无文件服务代码;nginx 仅服务受控静态目录,无 alias |
无 | 审计确认,无需修复 |
2. 前端(Vue 3 + nginx)
审计结论:不构成 ComfyUI follow_symlinks 类文件读取漏洞。
- nginx(
docker/nginx.conf.template)只服务受控的dist/静态目录,使用try_files, 无alias指令、无用户可控文件路径,不存在路径穿越面。 - Vue SPA 自身没有任何文件服务逻辑;文件下载全部走后端/算力节点 API。
- 前端 axios 拦截器(
frontend/src/api/request.ts)对每个请求自动附加Authorization: Bearer platform-token-{user_id},因此给后端接口补鉴权不会影响页面功能。
3. 后端(FastAPI)
3.1 data_convert 输出文件名路径穿越(严重)
问题:backend/app/modules/data_convert/router.py
output_filename由请求体传入后原样入库,随后拼进output_dir / output_filename用于写/读/删:if output_path.exists(): output_path.unlink()→ 任意文件删除open(output_path, "a")→ 任意文件追加写download_result用FileResponse(output_path)→ 任意文件读取
- 整个 router 无任何鉴权依赖(后端无全局鉴权中间件),任意网络访问者可利用。
修复:
def _safe_output_filename(value: Any) -> str:
"""输出文件名白名单:拒绝 ../、/、\ 及控制字符,仅允许普通文件名。"""
name = str(value or "converted-data.jsonl").strip()
if (
not name
or name in {".", ".."}
or name != Path(name).name
or "/" in name
or "\\" in name
or any(ord(c) < 32 or ord(c) == 127 for c in name)
):
raise fail(400, "output filename must be a plain file name")
return name
def _task_output_path(task: dict[str, Any]) -> Path:
"""统一构造转换输出路径,始终位于任务 output 目录内。"""
return _output_dir(task["id"]) / _safe_output_filename(task.get("output_filename"))
create_task创建时即校验(恶意值直接 400)- 全部 4 处使用点(
upload_source_files自动转换、run_convert、download_result、import_as_dataset)统一改用_task_output_path(),历史任务同样受保护 - 全部 8 个
/data-convert端点补充current_user: dict = Depends(get_current_user)鉴权
功能影响:正常转换流程(前端 outputName + '.jsonl' 这类纯文件名)不受影响;
接口现在要求登录态,未登录调用返回 401。
4. FastAPI 文档开关使用说明(ENABLE_DOCS)
4.1 为什么需要这个开关
FastAPI 默认注册 3 个无需鉴权的路由,直接泄露全部 API 结构:
| 路由 | 说明 |
|---|---|
/docs |
Swagger UI 交互文档 |
/redoc |
ReDoc 文档 |
/openapi.json |
OpenAPI Schema(含全部接口、参数、模型定义) |
修复方式是:关闭时让 FastAPI 不注册这 3 个路由,访问一律返回 404,而不是返回空页面。
4.2 核心实现
关闭的本质是向 FastAPI(...) 传入三个 None 参数:
# docs 关闭时等价于:
FastAPI(
title=...,
docs_url=None, # /docs → 404
redoc_url=None, # /redoc → 404
openapi_url=None, # /openapi.json → 404
)
4.3 后端开关逻辑(backend/app/core/config.py + backend/app/main.py)
# config.py —— Settings.enable_docs 在 __post_init__ 中计算
object.__setattr__(
self,
"enable_docs",
_bool_env("ENABLE_DOCS", os.getenv("APP_ENV", "local") != "prod"),
)
# config.py —— 返回传给 FastAPI 的文档参数
def docs_kwargs(enabled: bool) -> dict[str, Any]:
if enabled:
return {}
return {"docs_url": None, "redoc_url": None, "openapi_url": None}
# main.py —— 接入
app = FastAPI(title=settings.app_name, **docs_kwargs(settings.enable_docs))
判定顺序(优先级从高到低):
- 显式设置
ENABLE_DOCS=true/false→ 以显式值为准 - 未设置 →
APP_ENV != "prod"时开放,APP_ENV=prod时关闭
注意:
enable_docs从运行时环境读取APP_ENV(而非类定义时缓存的默认值), 确保生产环境默认关闭始终生效且便于测试。
4.4 算力节点开关逻辑(compute/api/security.py + compute/api/main.py)
# security.py
def docs_enabled() -> bool:
raw = os.getenv("ENABLE_DOCS", "").strip().lower()
if raw in {"true", "false"}:
return raw == "true"
auth_enabled = os.getenv("COMPUTE_AUTH_ENABLED", "true").lower() == "true"
return not auth_enabled # 开启 token 鉴权(生产)时默认关闭文档
def docs_kwargs() -> dict[str, Any]:
if docs_enabled():
return {}
return {"docs_url": None, "redoc_url": None, "openapi_url": None}
# main.py
app = FastAPI(title="YG Zhilian Compute API", **docs_kwargs())
判定顺序(优先级从高到低):
- 显式设置
ENABLE_DOCS=true/false→ 以显式值为准 - 未设置 →
COMPUTE_AUTH_ENABLED=true(生产默认)时关闭;COMPUTE_AUTH_ENABLED=false(本地开发)时开放
4.5 环境变量速查表
| 服务 | 环境变量 | 取值 | 默认行为 |
|---|---|---|---|
| 后端 | ENABLE_DOCS |
true / false |
未设置时按 APP_ENV != "prod" 判定 |
| 后端 | APP_ENV |
local / prod 等 |
prod 时关闭文档 |
| 算力节点 | ENABLE_DOCS |
true / false |
未设置时按 COMPUTE_AUTH_ENABLED 判定 |
| 算力节点 | COMPUTE_AUTH_ENABLED |
true / false |
true 时关闭文档 |
4.6 Docker 部署配置
已在以下文件加入 ENABLE_DOCS=false,并通过 docker-compose 透传(默认 false):
docker/app/.env → ENABLE_DOCS=false
docker/compute/.env → ENABLE_DOCS=false
docker/compute/.env.example → ENABLE_DOCS=false
docker/app/docker-compose.yml → ENABLE_DOCS: ${ENABLE_DOCS:-false}
docker/compute/docker-compose.yml → ENABLE_DOCS: ${ENABLE_DOCS:-false}
4.7 如何临时开启(排查/调试)
# 后端:非 prod 环境默认已开启;prod 环境临时开启
ENABLE_DOCS=true docker compose -f docker/app/docker-compose.yml up -d backend-api
# 算力节点:临时开启(生产默认关闭)
ENABLE_DOCS=true docker compose -f docker/compute/docker-compose.yml up -d compute-api
⚠️ 仅在可信内网调试时开启,用毕改回
false。
4.8 验证方法
# 关闭状态下三个地址均应返回 404
curl -s -o /dev/null -w "%{http_code}\n" http://<host>/docs # 404
curl -s -o /dev/null -w "%{http_code}\n" http://<host>/redoc # 404
curl -s -o /dev/null -w "%{http_code}\n" http://<host>/openapi.json # 404
# 健康检查不受影响
curl -s http://<host>/modelTF/health
5. 算力节点(FastAPI)
5.1 文档开关
见 §4.4,逻辑与后端一致,
生产(COMPUTE_AUTH_ENABLED=true)默认关闭。
补充:原 token 鉴权中间件已覆盖全部非 health 路径;现在文档路由同时被 FastAPI 层关闭, 属于纵深防御(双重保护)。
5.2 download_file glob 穿越 + 符号链接跟随(compute/api/main.py)
问题:
matches = list(upload_root.glob(f"{file_id}_*")) # file_id 来自 URL,直接拼进 glob
return FileResponse(matches[0]) # 跟随符号链接
Path.glob支持..段,file_id注入../可穿越出 upload 目录(已实测确认)- Linux 上目录内符号链接可被
FileResponse跟随 → 读取任意文件
修复:
# 1) file_id 字符白名单:仅字母/数字/_/-,含 .、/、% 等一律 400
if not file_id or not all(c.isalnum() or c in {"_", "-"} for c in file_id):
raise HTTPException(status_code=400, detail="invalid file id")
matches = list(upload_root.glob(f"{file_id}_*"))
if not matches:
raise HTTPException(status_code=404, detail="file not found")
# 2) 解析符号链接后必须仍位于 upload 根目录内
resolved = matches[0].resolve()
if not _path_inside(upload_root, resolved):
raise HTTPException(status_code=404, detail="file not found")
return FileResponse(resolved)
功能影响:服务端生成的 file_<时间戳> 格式完全兼容;外部工具使用正常 file_id 下载不受影响。
5.3 已确认安全的同类文件端点(无需改动)
| 端点 | 保护机制 |
|---|---|
compute/files/list |
_path_inside() + .resolve(),符号链接逃逸被阻断 |
compute/files/read |
同上 |
compute/files/upload |
同上 + 文件名取 .name |
compute/files/import-local |
目标路径 _path_inside() 校验 |
后端 data-process 存储 LocalDataProcessStorage |
lstat + S_ISLNK + O_NOFOLLOW + 规范化引用校验,彻底防符号链接 |
6. 测试与验证结果
| 验证项 | 结果 |
|---|---|
| 计算节点全量测试 | 22 passed, 1 skipped(跳过项为 Windows 无权限建符号链接,Linux 生产环境会执行) |
| 新增 compute 下载安全测试 | 正常下载 200;穿越样本 400/404;符号链接逃逸 404 |
| 新增后端 data_convert 安全测试 | 13 passed(穿越样本 9 项全拦截 + 鉴权覆盖检查) |
| 后端文档开关测试 | 6 passed(2 项 create_app 集成测试需完整依赖,在 WSL 下运行) |
| 实时验证 | 生产环境 /docs /redoc /openapi.json 均返回 404;download_file 合法 file_123456 返回 200 |
7. 变更文件清单
后端
backend/app/core/config.py— 新增_bool_env、docs_kwargs()、Settings.enable_docsbackend/app/main.py—FastAPI(...)接入docs_kwargsbackend/app/modules/data_convert/router.py— 输出文件名白名单 + 全部端点补鉴权
算力节点
compute/api/security.py— 新增文档开关模块(docs_enabled/docs_kwargs)compute/api/main.py— 文档开关接入 +download_file加固
Docker 配置
docker/app/.env、docker/compute/.env、docker/compute/.env.example—ENABLE_DOCS=falsedocker/app/docker-compose.yml、docker/compute/docker-compose.yml— 透传ENABLE_DOCS
测试
backend/tests/test_docs_security.py、backend/tests/test_data_convert_security.pycompute/tests/test_security.py、compute/tests/test_file_download_security.py