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

291 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 安全加固总结(前端 / 后端 / 算力节点)
> 记录 2026-08-06 对本平台的漏洞修复。核心目标:修复 **FastAPI 文档接口未授权访问**、
> **Swagger 泄露 API 结构**、以及两类**任意文件读取**漏洞(路径穿越 + 符号链接跟随),
> 修复过程不改变正常业务流程。
>
> 其中 **FastAPI 文档开关(`ENABLE_DOCS`** 的详细用法见
> [§4 FastAPI 文档开关使用说明](#4-fastapi-文档开关使用说明enabledocs)。
---
## 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 **无任何鉴权依赖**(后端无全局鉴权中间件),任意网络访问者可利用。
**修复**
```python
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` 参数:
```python
# 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`
```python
# 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`
```python
# 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 如何临时开启(排查/调试)
```bash
# 后端:非 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 验证方法
```bash
# 关闭状态下三个地址均应返回 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](#44-算力节点开关逻辑computeapisecuritypy--computeapimainpy),逻辑与后端一致,
生产(`COMPUTE_AUTH_ENABLED=true`)默认关闭。
> 补充:原 token 鉴权中间件已覆盖全部非 health 路径;现在文档路由同时被 FastAPI 层关闭,
> 属于**纵深防御**(双重保护)。
### 5.2 `download_file` glob 穿越 + 符号链接跟随(`compute/api/main.py`
**问题**
```python
matches = list(upload_root.glob(f"{file_id}_*")) # file_id 来自 URL直接拼进 glob
return FileResponse(matches[0]) # 跟随符号链接
```
- `Path.glob` 支持 `..` 段,`file_id` 注入 `../` 可**穿越出 upload 目录**(已实测确认)
- Linux 上目录内符号链接可被 `FileResponse` 跟随 → 读取任意文件
**修复**
```python
# 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` 均返回 **404**`download_file` 合法 `file_123456` 返回 200 |
---
## 7. 变更文件清单
**后端**
- `backend/app/core/config.py` — 新增 `_bool_env``docs_kwargs()``Settings.enable_docs`
- `backend/app/main.py``FastAPI(...)` 接入 `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/.env``docker/compute/.env``docker/compute/.env.example``ENABLE_DOCS=false`
- `docker/app/docker-compose.yml``docker/compute/docker-compose.yml` — 透传 `ENABLE_DOCS`
**测试**
- `backend/tests/test_docs_security.py``backend/tests/test_data_convert_security.py`
- `compute/tests/test_security.py``compute/tests/test_file_download_security.py`