Files
YG_FT/docs/security-hardening.md
wuyongtao 75cc105ebc chore: 忽略离线部署包,提交安全加固、数据库初始化与文档
- .gitignore: 忽略 docker/offline 离线部署包(镜像/运行时等大文件)
- 安全加固: 新增 compute/api/security.py 及各端安全测试,补充 docs/security-hardening.md
- 数据库: 新增完整初始化 SQL 与 docs/database-config.md
- 数据转换与评测: 修复类型检查、增强校验并补充测试
- Docker 配置与环境变量更新

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 09:24:35 +08:00

12 KiB
Raw Blame History

安全加固总结(前端 / 后端 / 算力节点)

记录 2026-08-06 对本平台的漏洞修复。核心目标:修复 FastAPI 文档接口未授权访问Swagger 泄露 API 结构、以及两类任意文件读取漏洞(路径穿越 + 符号链接跟随), 修复过程不改变正常业务流程。

其中 FastAPI 文档开关(ENABLE_DOCS 的详细用法见 §4 FastAPI 文档开关使用说明


1. 漏洞总览

# 影响面 漏洞 风险等级 修复
1 后端 + 算力节点 FastAPI 默认暴露 /docs/redoc/openapi.json未授权泄露全部 API 结构、参数、内部路由 生产环境关闭文档路由,访问返回 404ENABLE_DOCS 可覆盖)
2 后端 data-convert 模块 output_filename 路径穿越:可任意文件读 / 写 / 删,且整个模块无鉴权 严重 输出文件名白名单校验 + 全部端点补鉴权
3 算力节点 compute/files/{file_id}/downloadfile_id 直接拼进 glob 模式可 ../ 穿越出上传目录FileResponse 在 Linux 上跟随符号链接读取任意文件 中高 file_id 字符白名单 + 解析后路径包含性二次校验
4 前端Vue + nginx 无文件服务代码nginx 仅服务受控静态目录,无 alias 审计确认,无需修复

2. 前端Vue 3 + nginx

审计结论:不构成 ComfyUI follow_symlinks 类文件读取漏洞。

  • nginxdocker/nginx.conf.template)只服务受控的 dist/ 静态目录,使用 try_filesalias 指令、无用户可控文件路径,不存在路径穿越面。
  • 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_resultFileResponse(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_convertdownload_resultimport_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))

判定顺序(优先级从高到低)

  1. 显式设置 ENABLE_DOCS=true/false → 以显式值为准
  2. 未设置 → 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 Fine-Tune Compute API", **docs_kwargs())

判定顺序(优先级从高到低)

  1. 显式设置 ENABLE_DOCS=true/false → 以显式值为准
  2. 未设置 → 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 passed2 项 create_app 集成测试需完整依赖,在 WSL 下运行)
实时验证 生产环境 /docs /redoc /openapi.json 均返回 404download_file 合法 file_123456 返回 200

7. 变更文件清单

后端

  • backend/app/core/config.py — 新增 _bool_envdocs_kwargs()Settings.enable_docs
  • backend/app/main.pyFastAPI(...) 接入 docs_kwargs
  • backend/app/modules/data_convert/router.py — 输出文件名白名单 + 全部端点补鉴权

算力节点

  • compute/api/security.py — 新增文档开关模块(docs_enabled / docs_kwargs
  • compute/api/main.py — 文档开关接入 + download_file 加固

Docker 配置

  • docker/app/.envdocker/compute/.envdocker/compute/.env.exampleENABLE_DOCS=false
  • docker/app/docker-compose.ymldocker/compute/docker-compose.yml — 透传 ENABLE_DOCS

测试

  • backend/tests/test_docs_security.pybackend/tests/test_data_convert_security.py
  • compute/tests/test_security.pycompute/tests/test_file_download_security.py