Spaces:
Running
Running
sync from GitHub 75f7da8: security: comprehensive audit report (round 2) - 14 new findings
Browse files- security_best_practices_report.md +440 -180
security_best_practices_report.md
CHANGED
|
@@ -1,270 +1,530 @@
|
|
| 1 |
-
# XTC Backend (Hugging Face) 安全审计
|
| 2 |
|
| 3 |
- 审计时间:2026-07-21
|
| 4 |
-
- 审计对象:`xtc-backend-hf`(FastAPI + SQLite + HF Hub 备份)
|
| 5 |
-
- 审计范围:
|
| 6 |
-
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
---
|
| 9 |
|
| 10 |
-
##
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
| 17 |
|
| 18 |
---
|
| 19 |
|
| 20 |
-
##
|
| 21 |
|
| 22 |
-
###
|
| 23 |
|
| 24 |
**位置**
|
| 25 |
-
- `app/
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
**风险**
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
**
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
-
|
| 44 |
-
-
|
| 45 |
-
- `
|
| 46 |
-
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
``
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
```
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
---
|
| 54 |
|
| 55 |
-
###
|
| 56 |
|
| 57 |
-
**位置**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
**风险**
|
| 60 |
-
攻击者可
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
- `
|
| 65 |
-
- 重
|
| 66 |
-
|
| 67 |
-
**修复**
|
| 68 |
-
-
|
| 69 |
-
-
|
| 70 |
-
-
|
| 71 |
-
-
|
| 72 |
-
- 重定向上限:3 次
|
| 73 |
-
- 大小限制:`Content-Length` 与实际 `len(raw)` 双校验,上限 20MB(防内存爆破)
|
| 74 |
-
- 输入长度限制:`body.image` 长度上限 8192
|
| 75 |
-
- 错误信息收敛:`fix_image_bytes` 异常不透出 `str(exc)`,只回固定文案 + 服务端 `logger.exception`
|
| 76 |
|
| 77 |
---
|
| 78 |
|
| 79 |
-
##
|
| 80 |
|
| 81 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
**风险**
|
| 84 |
-
- 任意
|
| 85 |
-
-
|
| 86 |
-
-
|
| 87 |
-
-
|
| 88 |
|
| 89 |
-
**修复**
|
| 90 |
-
-
|
| 91 |
-
-
|
| 92 |
-
-
|
| 93 |
-
-
|
| 94 |
|
| 95 |
---
|
| 96 |
|
| 97 |
-
###
|
| 98 |
-
|
| 99 |
-
**位置**:`app/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
**风险**
|
| 102 |
-
|
| 103 |
-
-
|
| 104 |
-
-
|
| 105 |
-
-
|
| 106 |
-
- 第三方库版本与端点信息(`httpx.ConnectError: ...`)
|
| 107 |
|
| 108 |
-
攻击者
|
| 109 |
|
| 110 |
-
**修复**
|
| 111 |
-
-
|
| 112 |
-
-
|
| 113 |
-
-
|
| 114 |
|
| 115 |
---
|
| 116 |
|
| 117 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
**风险**
|
| 124 |
-
-
|
| 125 |
-
-
|
| 126 |
-
- 攻击者
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
-
|
| 131 |
-
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
3. **per-username 限流**:每用户名 60 秒最多 5 次登录尝试(防针对单一账号爆破)
|
| 135 |
-
- `record_auth_failure(ip, username)`:记录失败,达阈值时触发锁定
|
| 136 |
-
- `clear_auth_failures(ip, username)`:登录成功后清零失败计数
|
| 137 |
-
|
| 138 |
-
**接入点**
|
| 139 |
-
- `app/middleware.py` `RateLimitMiddleware`:鉴权路径走 `check_auth_rate_limit`,POST 方法才计入,429 响应带 `Retry-After` / `X-RateLimit-Scope`
|
| 140 |
-
- `app/api/user_files.py` `register`:用户名冲突 → `record_auth_failure`;成功 → `clear_auth_failures`
|
| 141 |
-
- `app/api/user_files.py` `login`:用户不存在或密码错误 → `record_auth_failure`(不区分两种情况,防账号枚举);成功 → `clear_auth_failures`
|
| 142 |
-
- `app/api/admin.py` `login`:admin key 校验失败 → `record_auth_failure`;成功 → `clear_auth_failures`
|
| 143 |
|
| 144 |
---
|
| 145 |
|
| 146 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
**修复**
|
| 151 |
-
-
|
| 152 |
-
-
|
| 153 |
-
-
|
|
|
|
| 154 |
|
| 155 |
---
|
| 156 |
|
| 157 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
**风险**
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
---
|
| 167 |
|
| 168 |
-
##
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
**位置**
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
**风险**
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
**修复**
|
| 176 |
-
-
|
| 177 |
-
-
|
| 178 |
-
- 密钥长度 < 16
|
| 179 |
-
- CORS 为通配模式
|
| 180 |
-
- Admin 页面无门禁 token
|
| 181 |
|
| 182 |
---
|
| 183 |
|
| 184 |
-
###
|
| 185 |
|
| 186 |
-
**位置**:`.
|
| 187 |
|
| 188 |
-
**
|
| 189 |
-
-
|
| 190 |
-
-
|
|
|
|
|
|
|
| 191 |
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
|
| 196 |
-
###
|
| 197 |
|
| 198 |
-
**位置**:`app/
|
| 199 |
|
| 200 |
-
**
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
-
|
| 205 |
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
---
|
| 209 |
|
| 210 |
-
##
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
-
|
| 214 |
-
- 时间常量比较(`_consteq`)防 admin key 时序攻击
|
| 215 |
-
- SQLite WAL 模式 + 后台写线程(`db_writer`)避免事件循环阻塞
|
| 216 |
-
- 参数化 SQL 全程使用 `?` 占位符(无字符串拼接 SQL 注入面)
|
| 217 |
-
- 扫描器拦截中间件(`ScannerBlockMiddleware`):检测扫描行为拉黑 IP 5 分钟
|
| 218 |
-
- 自动文档端点关闭(`docs_url=None`、`redoc_url=None`、`openapi_url=None`)
|
| 219 |
-
- `/admin` 页面门禁(`XTC_ADMIN_ACCESS_TOKEN`):未配置 token 时启动告警
|
| 220 |
-
- HF Hub 配置备份脱敏:日志只回 `***<last4>`
|
| 221 |
|
| 222 |
---
|
| 223 |
|
| 224 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
-
|
| 229 |
-
-
|
| 230 |
-
- [ ] `XTC_JWT_SECRET` 已改为 ≥32 字符随机串
|
| 231 |
-
- [ ] `XTC_CORS_ORIGINS` 已配置为前端实际域名(逗号分隔多个)
|
| 232 |
-
- [ ] `XTC_ADMIN_ACCESS_TOKEN` 已配置(给 `/admin` 页面加门禁)
|
| 233 |
-
- [ ] `XTC_DISABLE_ADMIN=false`(除非确实要关闭后台)
|
| 234 |
-
- [ ] 启动日志中无 `[startup] ... is EMPTY / still the default value / too short` 告警
|
| 235 |
-
- [ ] 启动日志中无 `XTC_CORS_ORIGINS not set` 告警
|
| 236 |
-
- [ ] `.env` 文件未提交到代码仓库(已在 `.gitignore` 中)
|
| 237 |
-
- [ ] HF_TOKEN(若启用 Hub 备份)为只读或限定仓库的细粒度 token
|
| 238 |
|
| 239 |
---
|
| 240 |
|
| 241 |
-
##
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
| `app/api/admin.py` | 修改 | admin 登录失败跟踪 |
|
| 254 |
-
| `app/api/user_files.py` | 修改 | 注册/登录失败跟踪 |
|
| 255 |
-
| `app/services/rate_limit_store.py` | 修改 | 鉴权限流模块 / 失败锁定 |
|
| 256 |
-
| `app/services/request_log_store.py` | 修改 | 敏感标头脱敏扩充 |
|
| 257 |
-
| `.env.example` | 修改 | 安全提示 / `XTC_CORS_ORIGINS` 配置 |
|
| 258 |
-
| `security_best_practices_report.md` | 新增 | 本报告 |
|
| 259 |
|
| 260 |
---
|
| 261 |
|
| 262 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
- 给 `/v1/*` 业务接口加 per-device_id 限流维度(已有 per-key/per-ip)
|
| 266 |
-
- 把鉴权失败计数持久化到 SQLite,避免重启后丢失锁定状态
|
| 267 |
-
- 给 admin 接口加二次认证(TOTP / WebAuthn)
|
| 268 |
-
- 上游 API Key 轮换监控(keypool 健康检查)
|
| 269 |
-
- 引入 CSP / HSTS / X-Content-Type-Options 等安全响应头(需在反向代理层配置)
|
| 270 |
-
- 定期备份 SQLite 到 HF Hub(已有 user_files 备份,可扩展到全库)
|
|
|
|
| 1 |
+
# XTC Backend (Hugging Face) 安全审计报告(第二轮)
|
| 2 |
|
| 3 |
- 审计时间:2026-07-21
|
| 4 |
+
- 审计对象:`xtc-backend-hf`(FastAPI 0.115.6 + SQLite + HF Hub 备份 + JWT HS256 + bcrypt)
|
| 5 |
+
- 审计范围:跨域 / 鉴权 / 跨用户文件 / Token 泄露 / 管理面板 / 数据库 / 日志 / DoS / 信息泄露
|
| 6 |
+
- 审计方法:基于 OWASP API Top 10 (2023) + FastAPI 安全规范(FASTAPI-* 规则集)逐文件人工审查
|
| 7 |
+
- 参考规范:`/data/user/skills/security-best-practices/references/python-fastapi-web-server-security.md`
|
| 8 |
+
- 第一轮报告基线:`security_best_practices_report.md`(C1-C4 / H1-H5 / L1-L2 已修复,本轮不再重复)
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
+
## 0) 执行摘要(Executive Summary)
|
| 13 |
|
| 14 |
+
本轮重点对第一轮未覆盖的端点、中间件、Token 流转链路做深度审查,新发现 **3 个 Critical、4 个 High、4 个 Medium、3 个 Low** 共 14 项问题。最严重的 3 项分别是:
|
| 15 |
+
|
| 16 |
+
- **NC1**:用户 JWT 与 admin 页面门禁 token 均可通过 URL query 参数传递,会在浏览器历史、Referer、server access log、CDN 日志、Webhook 等多处长期泄露,令牌一旦泄露即可在 7 天 TTL 内被任意利用。
|
| 17 |
+
- **NC2**:当部署时未配置 `XTC_ACCESS_KEY` 时,`verify_access_key` 直接放行,**整套 `/v1/*` 业务接口处于完全无鉴权状态**,所有用户的对话、Token 用量、上游 API Key 池都可被任意未授权访问。
|
| 18 |
+
- **NC3**:管理员可通过 `/admin/api/files/{key}/download` 下载任意用户的文件/备份内容、通过 `/admin/api/request-log` 查看任意用户的对话明文,**且无任何审计日志记录**,admin key 一旦泄露即等于全量数据失陷且无可追溯。
|
| 19 |
+
|
| 20 |
+
所有发现均按 OWASP API Top 10 (2023) 类别归类,详见 §一至 §四。
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
+
## 一、Critical 级别问题
|
| 25 |
|
| 26 |
+
### NC1. Token 通过 URL Query 参数传递 → 多通道泄露(FASTAPI-AUTH-002)
|
| 27 |
|
| 28 |
**位置**
|
| 29 |
+
- `app/api/user_files.py:130-140` `_extract_token()`:
|
| 30 |
+
```python
|
| 31 |
+
def _extract_token(request: Request) -> str:
|
| 32 |
+
# 优先 query 参数(小天才手表等平台会拦截自定义 header,只能走 query)
|
| 33 |
+
q = request.query_params.get("token") or ""
|
| 34 |
+
q = q.strip()
|
| 35 |
+
if q:
|
| 36 |
+
return q
|
| 37 |
+
...
|
| 38 |
+
```
|
| 39 |
+
- `app/admin_html.py:19-35` `admin_page()`:以 `?k=<token>` 作为 `/admin` 页面门禁
|
| 40 |
+
```python
|
| 41 |
+
@router.get("/admin", response_class=HTMLResponse)
|
| 42 |
+
async def admin_page(request: Request, k: str | None = Query(default=None)) -> Response:
|
| 43 |
+
settings = get_settings()
|
| 44 |
+
if settings.has_admin_access_token:
|
| 45 |
+
provided = (k or "").strip()
|
| 46 |
+
expected = settings.xtc_admin_access_token
|
| 47 |
+
if not provided or not secrets.compare_digest(provided, expected):
|
| 48 |
+
return JSONResponse(status_code=404, ...)
|
| 49 |
+
```
|
| 50 |
+
- 用户 JWT 默认 TTL = 7 天(`app/api/user_files.py:51` `TOKEN_TTL_SEC = 7 * 24 * 3600`)
|
| 51 |
|
| 52 |
**风险**
|
| 53 |
+
长生命周期的 access token(用户 JWT 7 天、`XTC_ADMIN_ACCESS_TOKEN` 长期有效)通过 URL query 传递时,会在以下通道泄露:
|
| 54 |
+
1. **浏览器历史记录**:用户/管理员分享设备时被读取
|
| 55 |
+
2. **Referer 头**:页面加载外部资源(图片、字体、CDN)时被带到第三方
|
| 56 |
+
3. **服务器 access log**:`uvicorn.access` 默认记录完整 URL(含 query string)
|
| 57 |
+
4. **反向代理 / CDN 日志**:Cloudflare、nginx 默认记录 query
|
| 58 |
+
5. **Webhook 回调**:上游报错时把请求 URL 拼进 error message 回传
|
| 59 |
+
6. **HF Space 内部链路**:HF 平台访问日志、`x-direct-url` 链路日志
|
| 60 |
+
|
| 61 |
+
由于 admin token 长期有效、用户 JWT TTL 7 天,泄露窗口极长。攻击者拿到 token 后可冒充该用户上传/下载/删除文件、阅读会话;拿到 admin access token 后可直接进入 `/admin` 登录页(虽然还需 admin key 才能调 API,但已大幅压缩攻击面)。
|
| 62 |
+
|
| 63 |
+
**修复建议**
|
| 64 |
+
- **用户 JWT**:
|
| 65 |
+
- 优先保留 `Authorization: Bearer <token>` 头与 `x-xtc-user-token` 头两种方式
|
| 66 |
+
- `?token=` query 参数仅在确认客户端平台(小天才手表)无法发 header 时才允许,且:
|
| 67 |
+
- 缩短通过 query 路径签发的 token TTL(例如 5 分钟)
|
| 68 |
+
- 在响应头加 `Cache-Control: no-store` 防中间缓存
|
| 69 |
+
- access log 中过滤 `?token=` 参数(参考 `_SENSITIVE_BODY_KEYS` 机制)
|
| 70 |
+
- **Admin 页面门禁 token**:完全废弃 `?k=<token>` query 形式,改为:
|
| 71 |
+
- 用一个 **短期一次性 nonce**:`GET /admin?n=<nonce>` → 服务端验证 nonce 后下发 `HttpOnly; Secure; SameSite=Strict` Cookie,后续 `/admin` 凭 Cookie 访问
|
| 72 |
+
- 或者把门禁 token 放到 `Authorization` 头(前端用 fetch 拦截器统一加)
|
| 73 |
+
- **TTL 收紧**:用户 JWT 7 天 TTL 过长,建议改为 1 小时 + refresh token(已有 `/u/api/refresh` 端点,前端需主动调用)
|
| 74 |
+
- **吊销列表**:用户 JWT 当前无服务端吊销机制(access_tokens 表只覆盖 admin 签发的临时令牌),泄露后只能等过期
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
### NC2. 未配置 `XTC_ACCESS_KEY` 时整套 `/v1/*` 业务接口完全无鉴权(FASTAPI-AUTH-001)
|
| 79 |
+
|
| 80 |
+
**位置**:`app/auth.py:46-68` `verify_access_key()`
|
| 81 |
+
```python
|
| 82 |
+
def verify_access_key(provided: Optional[str]) -> None:
|
| 83 |
+
settings = get_settings()
|
| 84 |
+
if not settings.has_access_key:
|
| 85 |
+
# 未配置 access_key 时放行(开发模式)
|
| 86 |
+
return
|
| 87 |
+
if not provided:
|
| 88 |
+
raise HttpError(...)
|
| 89 |
+
...
|
| 90 |
```
|
| 91 |
|
| 92 |
+
**风险**
|
| 93 |
+
HF Space 默认环境变量中不包含 `XTC_ACCESS_KEY`,若部署时遗漏配置:
|
| 94 |
+
- `/v1/chat/completions`、`/v1/xtc/*`、`/v1/models` 等业务接口完全无鉴权
|
| 95 |
+
- 任意匿名用户可消耗上游 Gemini / OpenAI API Key 额度(直接经济损失)
|
| 96 |
+
- 上游 API Key 池(admin 配置的 provider api_keys)可被未授权用户滥用
|
| 97 |
+
- 用户文件接口 `/u/api/*` 虽有独立 JWT 鉴权不受影响,但 `/v1/*` 业务路径全部失守
|
| 98 |
+
- `_warn_insecure_secrets()`(H4 修复)只在启动日志告警,不阻断请求,部署者容易忽略
|
| 99 |
+
|
| 100 |
+
**修复建议**
|
| 101 |
+
- 生产环境必须强制配置 `XTC_ACCESS_KEY`:在 `lifespan` 启动时若检测到 `ENV=production` 且 `has_access_key=False`,直接 `raise RuntimeError` 拒绝启动
|
| 102 |
+
- 引入显式的 `XTC_DEV_MODE=1` 开关,仅在显式开启时才允许 `has_access_key=False` 放行
|
| 103 |
+
- 启动告警升级为邮件/飞书/Webhook 通知(而不只是 `logging.warning`)
|
| 104 |
+
|
| 105 |
---
|
| 106 |
|
| 107 |
+
### NC3. Admin 跨用户数据访问无审计 → Admin Key 泄露即全量失陷且无追溯(FASTAPI-AUTHZ-001 / OWASP API1:2023)
|
| 108 |
|
| 109 |
+
**位置**
|
| 110 |
+
- `app/api/admin_data.py:378-417` `download_any_file()`:管理员下载任意用户文件/备份内容,无 audit log
|
| 111 |
+
```python
|
| 112 |
+
@router.get("/files/{key}/download")
|
| 113 |
+
async def download_any_file(key: str, _admin: str = Depends(_require_admin)):
|
| 114 |
+
...
|
| 115 |
+
return Response(content=content, ...)
|
| 116 |
+
# ⚠️ 整个函数无 audit_log_store.append(...) 调用
|
| 117 |
+
```
|
| 118 |
+
- `app/api/admin_data.py:470-488` `delete_any_file()`:管理员删除任意用户文件,仅 `logger.info` 不入审计表
|
| 119 |
+
```python
|
| 120 |
+
logger.info("[admin delete] key=%s kind=%s owner=%s", key, kind, owner)
|
| 121 |
+
```
|
| 122 |
+
- `app/api/admin_data.py:547-600` `delete_user()`:级联删除用户全部数据,仅 `logger.info`
|
| 123 |
+
- `app/services/request_log_store.py:243-302` `list_records()`:admin 可查询任意用户的对话内容明文(`request_body` / `response_body`),无审计
|
| 124 |
+
- `app/request_log_middleware.py:31` `_RECORDED_PREFIXES = ("/v1/", "/admin/api/", "/u/api/")`:**用户文件 API `/u/api/*` 的请求体也被记录到 SQLite**(含登录密码字段虽被脱敏,但 `/u/api/upload` 的 multipart 被标 `[multipart form-data]`,`/u/api/files` 等不含敏感数据;不过 `/u/api/login` 的 body 中的 `password` 字段在 `_SENSITIVE_BODY_KEYS` 中已脱敏,**但 `/v1/*` 业务请求中用户对话原文被完整保存**)
|
| 125 |
|
| 126 |
**风险**
|
| 127 |
+
- Admin key 一旦泄露(如通过 NC1 的 query 参数泄露、或浏览器 localStorage 被读取、或弱密码被爆破),攻击者可:
|
| 128 |
+
1. 下载任意用户上传的私人文件(`/admin/api/files/{key}/download`)
|
| 129 |
+
2. 查看任意用户的完整对话历史(`/admin/api/request-log` + `/admin/api/sessions/{id}/messages`)
|
| 130 |
+
3. 删除任意用户的所有数据且无可追溯(无 audit log)
|
| 131 |
+
- 由于 `audit_log` 表已存在但仅 `config_store` 等少数路径调用,admin 数据 API 完全未接入审计
|
| 132 |
+
- HF Space 单实例 + 无 SIEM,文件级 `logger.info` 在 Space 重建后丢失,等同于"无审计"
|
| 133 |
+
|
| 134 |
+
**修复建议**
|
| 135 |
+
- 所有 `/admin/api/files/{key}/download`、`/admin/api/files/{key}` (DELETE)、`/admin/api/users/{user_id}` (DELETE)、`/admin/api/sessions/{id}` (DELETE) 路径必须调用 `audit_log_store.append(action="admin.download_file", actor=admin_key_hash, target=key, detail={owner, namespace, size})`
|
| 136 |
+
- 对 `/admin/api/request-log` 查询接口加审计:记录哪个 admin 查看了哪条 request_log
|
| 137 |
+
- 对 admin 操作的 audit_log 单独加表或加 `actor_type='admin'` 字段,防止 admin 自己清空 audit_log 表
|
| 138 |
+
- 在 `request_log_middleware` 中对 `/u/api/*` 路径默认不记录请求体(用户文件接口无需日志回放),或仅记录元信息(method/path/status)不记 body
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
---
|
| 141 |
|
| 142 |
+
## 二、High 级别问题
|
| 143 |
|
| 144 |
+
### NH1. 用户对话原文以明文持久化到 SQLite,admin 可读且无脱敏(OWASP API2:2023)
|
| 145 |
+
|
| 146 |
+
**位置**
|
| 147 |
+
- `app/request_log_middleware.py:122-133`:业务请求体被解析后存入 `request_body` 字段
|
| 148 |
+
```python
|
| 149 |
+
if "application/json" in ct:
|
| 150 |
+
try:
|
| 151 |
+
req_body_redacted = request_log_store._redact_body(json.loads(request_body_raw))
|
| 152 |
+
...
|
| 153 |
+
```
|
| 154 |
+
- `app/services/request_log_store.py:59-71` `_redact_body()`:仅对 `_SENSITIVE_BODY_KEYS = {"api_key", "token", "password", ...}` 中的字段脱敏,**用户对话内容(messages 数组中的 user/assistant content)不在脱敏列表**
|
| 155 |
+
- `app/services/request_log_store.py:162-223` `insert()`:完整 body 写入 SQLite `request_log` 表
|
| 156 |
+
- `app/api/request_logs.py`(admin 接口):admin 可查询任意 request_log 详情,含完整 `request_body` / `response_body`
|
| 157 |
+
- `request_log` 表无用户隔离:所有用户的对话都混在同一张表,admin 查询无过滤
|
| 158 |
|
| 159 |
**风险**
|
| 160 |
+
- 任意用户的对话内容(可能含 PII:姓名、电话、地址、医疗/金融信息)以明文形式持久化在 `/data/xtc.db`
|
| 161 |
+
- DB 文件本身在 HF Space 容器内,若容器被入侵或 NC2 的无鉴权状态被滥用,攻击者可一次性获取全部历史对话
|
| 162 |
+
- admin(含合法 admin 与被盗 admin key 的攻击者)可无差别浏览所有用户对话
|
| 163 |
+
- `request_log_limit` 默认 500 条但可调至 5000,且不区分用户,足以重建任意用户画像
|
| 164 |
|
| 165 |
+
**修复建议**
|
| 166 |
+
- 对 `/v1/chat/completions`、`/v1/xtc/*` 等含对话内容的路径,`request_body` 字段在入库前对 `messages` 数组做哈希或截断(仅保留前 100 字符 + 长度)
|
| 167 |
+
- 把 `messages`、`content`、`prompt`、`text` 等对话相关字段加入 `_SENSITIVE_BODY_KEYS`,或单独引入 `_REDACTED_CONTENT_KEYS` 做部分脱敏(保留长度信息便于排障)
|
| 168 |
+
- admin 查看 request_log 详情时强制审计(参见 NC3)
|
| 169 |
+
- 提供用户级"清除我的对话历史"接口(GDPR/PIPL 合规)
|
| 170 |
|
| 171 |
---
|
| 172 |
|
| 173 |
+
### NH2. Admin Login 响应区分 401/403/500 → 状态枚举(OWASP API8:2023)
|
| 174 |
+
|
| 175 |
+
**位置**:`app/auth.py:169-191` `verify_admin_key()`
|
| 176 |
+
```python
|
| 177 |
+
def verify_admin_key(provided: Optional[str]) -> None:
|
| 178 |
+
settings = get_settings()
|
| 179 |
+
if not settings.is_admin_enabled:
|
| 180 |
+
raise HttpError("Admin API disabled (XTC_DISABLE_ADMIN=true)", status=403, code="forbidden")
|
| 181 |
+
if not settings.has_admin_key:
|
| 182 |
+
raise HttpError("XTC_ADMIN_KEY not configured", status=500, code="server_misconfigured")
|
| 183 |
+
if not provided:
|
| 184 |
+
raise HttpError("Missing admin key: ...", status=401, code="unauthorized")
|
| 185 |
+
if not _consteq(provided, settings.xtc_admin_key):
|
| 186 |
+
logger.warning("[auth] invalid admin key")
|
| 187 |
+
raise HttpError("Invalid admin key", status=401, code="unauthorized")
|
| 188 |
+
```
|
| 189 |
|
| 190 |
**风险**
|
| 191 |
+
匿名攻击者通过单次 POST `/admin/api/login` 即可区分三种部署状态:
|
| 192 |
+
- `403 forbidden` → admin 已禁用,无需进一步攻击
|
| 193 |
+
- `500 server_misconfigured` → admin 启用但 `XTC_ADMIN_KEY` 为空,可能配合 NC2 形成"admin 接口可调但无 key 校验"的链式利用(实际 `_require_admin` 仍会校验,但泄露了配置错误状态)
|
| 194 |
+
- `401 unauthorized` → admin 启用且配置了 key,可进入暴力破解流程
|
|
|
|
| 195 |
|
| 196 |
+
这种状态枚举帮助攻击者筛选目标实例(如批量扫描 HF Space 上的部署),降低攻击成本。
|
| 197 |
|
| 198 |
+
**修复建议**
|
| 199 |
+
- 对未鉴权请求统一返回 `401 unauthorized` + 相同错误文案("authentication required")
|
| 200 |
+
- 真实状态(disabled / misconfigured)只打到服务端日志
|
| 201 |
+
- 参考第一轮 H1 的"不区分用户不存在和密码错误"做法
|
| 202 |
|
| 203 |
---
|
| 204 |
|
| 205 |
+
### NH3. `_extract_access_key` 接受 Basic auth scheme → 凭据混淆(FASTAPI-AUTH-002)
|
| 206 |
+
|
| 207 |
+
**位置**:`app/auth.py:29-43` `_extract_access_key()`、`app/middleware.py:19-28`、`app/request_log_middleware.py:48-61`
|
| 208 |
+
```python
|
| 209 |
+
def _extract_access_key(*, authorization, x_access_key) -> Optional[str]:
|
| 210 |
+
if x_access_key:
|
| 211 |
+
return x_access_key.strip()
|
| 212 |
+
if authorization:
|
| 213 |
+
auth = authorization.strip()
|
| 214 |
+
if auth.lower().startswith("bearer "):
|
| 215 |
+
return auth[7:].strip()
|
| 216 |
+
if auth.lower().startswith("basic "):
|
| 217 |
+
return auth[6:].strip() # ⚠️ Basic auth 的 base64(user:pass) 被当作 access_key
|
| 218 |
+
return auth
|
| 219 |
+
```
|
| 220 |
|
| 221 |
+
**风险**
|
| 222 |
+
- `Authorization: Basic <base64>` 的语义是"用户名:密码"组合,被这里当作 access_key 整体使用,语义错位
|
| 223 |
+
- 攻击者可构造 `Authorization: Basic <实际 access_key 的 base64>` 来绕过部分 WAF / 日志规则(很多日志工具会自动脱敏 Basic 但不脱敏 Bearer)
|
| 224 |
+
- 在反代场景下,若上游 Basic 被透传,access_key 可能被错误地暴露在 `Authorization: Basic` 形式下被中间件记录
|
| 225 |
+
- access log 中 Basic auth 通常会被自动 base64 解码后脱敏用户名,导致 access_key 被以解码形式记录到日志
|
| 226 |
+
|
| 227 |
+
**修复建议**
|
| 228 |
+
- 移除 `if auth.lower().startswith("basic "):` 分支,仅接受 Bearer
|
| 229 |
+
- 若需兼容老客户端的"裸 token"形式,可保留 `return auth` 但仅限无 scheme 的情况
|
| 230 |
+
- 中间件与日志层级的 `_extract_access_key` 同步修改
|
| 231 |
+
|
| 232 |
+
---
|
| 233 |
|
| 234 |
+
### NH4. 备份分段上传 session 内存表无并发上限 → DoS(FASTAPI-LIMITS-001)
|
| 235 |
+
|
| 236 |
+
**位置**:`app/api/user_backups.py:55-73`
|
| 237 |
+
```python
|
| 238 |
+
MAX_PART_SIZE = 2 * 1024 * 1024
|
| 239 |
+
_UPLOAD_SESSION_TTL = 3600
|
| 240 |
+
# 内存存储:upload_id -> {user_id, username, alias, parts: {seq: bytes}, created_at}
|
| 241 |
+
_backup_upload_sessions: "dict[str, dict]" = {}
|
| 242 |
+
|
| 243 |
+
def _cleanup_expired_upload_sessions() -> None:
|
| 244 |
+
"""清理过期的分段上传 session,释放内存。每次创建新 session 时调用。"""
|
| 245 |
+
```
|
| 246 |
|
| 247 |
**风险**
|
| 248 |
+
- `_backup_upload_sessions` 字典无大小上限,每个用户可创建无限个 session
|
| 249 |
+
- 单 session 最大 50MB(`MAX_BACKUP_SIZE`),每 part 2MB;攻击者用合法 token 创建 N 个 session,每个塞 49MB 数据,可轻松耗尽 HF Space 容器内存(免费档通常 16GB)
|
| 250 |
+
- `_cleanup_expired_upload_sessions()` 仅在新建 session 时调用,若攻击者停止创建新 session,旧 session 会一直占内存直到 TTL(1 小时)到期
|
| 251 |
+
- 同样的问题存在于 `_backup_payload_cache: TTLCache(maxsize=32, ttl=60)`(per-user 32 条缓存),攻击者可通过批量下载触发缓存淘汰但每次仍占 60s 内存
|
| 252 |
+
|
| 253 |
+
**修复建议**
|
| 254 |
+
- 加 per-user 并发 session 上限:`MAX_SESSIONS_PER_USER = 3`,超出返回 429
|
| 255 |
+
- 加全局 session 上限:`MAX_TOTAL_SESSIONS = 1000`,超出触发清理或拒绝
|
| 256 |
+
- `_cleanup_expired_upload_sessions()` 改为后台定时任务(如每 60 秒)+ 每次创建前调用
|
| 257 |
+
- 在 `RateLimitMiddleware` 中对 `/u/api/backups/upload/session` 加更严格的 per-user 限流(如 60 秒 5 次)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
---
|
| 260 |
|
| 261 |
+
## 三、Medium 级别问题
|
| 262 |
+
|
| 263 |
+
### NM1. SQLite DB 文件无加密 + fallback 到 `/tmp/xtc.db` → 共享环境泄露(FASTAPI-FILES-001)
|
| 264 |
+
|
| 265 |
+
**位置**:`app/database.py:34-48` `_resolve_db_path()`
|
| 266 |
+
```python
|
| 267 |
+
def _resolve_db_path() -> str:
|
| 268 |
+
settings = get_settings()
|
| 269 |
+
db_path = settings.db_path # 默认 /data/xtc.db
|
| 270 |
+
try:
|
| 271 |
+
_ensure_parent_dir(db_path)
|
| 272 |
+
test_path = Path(db_path)
|
| 273 |
+
test_path.touch(exist_ok=True)
|
| 274 |
+
if not os.access(db_path, os.W_OK):
|
| 275 |
+
raise PermissionError(db_path)
|
| 276 |
+
return db_path
|
| 277 |
+
except Exception:
|
| 278 |
+
fallback = "/tmp/xtc.db"
|
| 279 |
+
logger.error("[database] db_path %s not writable, fallback to %s", db_path, fallback)
|
| 280 |
+
return fallback
|
| 281 |
+
```
|
| 282 |
|
| 283 |
+
**风险**
|
| 284 |
+
- `/data/xtc.db` 文件包含:用户密码哈希(bcrypt,但仍敏感)、admin key(明文,见下文)、所有用户文件元信息、对话历史(NH1)、上游 API Key(在 `config_store` 备份中可能含明文)
|
| 285 |
+
- DB 文件无加密,文件系统层面任意能读取该文件的进程/用户都可获取全部数据
|
| 286 |
+
- fallback 到 `/tmp/xtc.db`:`/tmp` 在多数 Linux 默认 1777 权限,**同主机其他用户可读取**(HF Space 容器虽隔离,但多用户共享主机场景下危险)
|
| 287 |
+
- DB 文件无独立访问控制(无 `chmod 600`)
|
| 288 |
|
| 289 |
+
**修复建议**
|
| 290 |
+
- DB 文件创建后立即 `os.chmod(db_path, 0o600)` 限制仅 owner 可读写
|
| 291 |
+
- 启动时若 fallback 到 `/tmp`,在 `lifespan` 中 `raise RuntimeError` 拒绝启动生产模式
|
| 292 |
+
- 考虑使用 SQLCipher 对 DB 加密(pip install pysqlcipher3),密钥从 `XTC_DB_KEY` 环境变量读取
|
| 293 |
+
- `XTC_ADMIN_KEY`、上游 API Key 等敏感配置不应持久化到 SQLite(当前 `config_store` 会备份到 Hub),应只保留在内存或加密存储
|
| 294 |
|
| 295 |
---
|
| 296 |
|
| 297 |
+
### NM2. 用户 JWT 7 天 TTL 过长 + 无服务端吊销机制(FASTAPI-AUTH-004)
|
| 298 |
+
|
| 299 |
+
**位置**:`app/api/user_files.py:51` `TOKEN_TTL_SEC = 7 * 24 * 3600`
|
| 300 |
+
```python
|
| 301 |
+
TOKEN_TTL_SEC = 7 * 24 * 3600 # 7 天
|
| 302 |
+
...
|
| 303 |
+
def _issue_user_token(user_id: int, username: str) -> str:
|
| 304 |
+
payload = {
|
| 305 |
+
"type": "user_file",
|
| 306 |
+
"sub": str(user_id),
|
| 307 |
+
"username": username,
|
| 308 |
+
"iat": now,
|
| 309 |
+
"exp": now + TOKEN_TTL_SEC,
|
| 310 |
+
}
|
| 311 |
+
return jwt.encode(payload, settings.xtc_jwt_secret, algorithm=ALGORITHM)
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
**风险**
|
| 315 |
+
- 7 天 TTL 远超一般业务需要,token 一旦泄露(参见 NC1 的 query 参数泄露)可在 7 天内被任意利用
|
| 316 |
+
- 用户 JWT **无服务端吊销列表**:`access_tokens` 表只用于 admin 签发的临时令牌(`type="access"`),用户 JWT(`type="user_file"`)不在表中,泄露后无法主动失效
|
| 317 |
+
- `/u/api/refresh` 端点(`user_files.py:362-370`)签发新 token 但不吊销旧 token,**旧 token 仍有效至过期**,相当于"双 token 并存"
|
| 318 |
+
- 用户改密码 / 注销账号后,旧 token 仍可用(注销账号 `_delete_account` 未清理 token,因为根本没有 token 表)
|
| 319 |
|
| 320 |
+
**修复建议**
|
| 321 |
+
- TTL 缩短至 1 小时,配合 refresh token 机制(refresh token 单独存储 + 可吊销)
|
| 322 |
+
- 引入 `user_tokens` 表持久化用户 JWT 的 `jti`,支持吊销
|
| 323 |
+
- 改密码 / 注销账号时批量吊销该用户所有未过期 token
|
| 324 |
+
- `/u/api/refresh` 应吊销旧 token 再签发新 token(token rotation)
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
### NM3. 扫描器拦截中间件 `_WHITELIST_PREFIXES` 死代码 → 误拦风险(FASTAPI-AUTH-001)
|
| 329 |
+
|
| 330 |
+
**位置**:`app/scanner_block_middleware.py:78-89, 168-225`
|
| 331 |
+
```python
|
| 332 |
+
_WHITELIST_PREFIXES = (
|
| 333 |
+
"/", "/health", "/v1/", "/admin", "/u/", "/uapi/", "/favicon.ico", "/robots.txt",
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
def _is_whitelisted_path(path: str) -> bool: # ⚠️ 函数定义但 dispatch 中从未调用
|
| 337 |
+
...
|
| 338 |
+
|
| 339 |
+
async def dispatch(self, request, call_next):
|
| 340 |
+
if request.method == "OPTIONS":
|
| 341 |
+
return await call_next(request)
|
| 342 |
+
client_ip = _get_client_ip(request)
|
| 343 |
+
path = request.url.path
|
| 344 |
+
if _is_internal_ip(client_ip):
|
| 345 |
+
return await call_next(request)
|
| 346 |
+
if _is_blacklisted(client_ip):
|
| 347 |
+
...
|
| 348 |
+
if _is_scanner_path(path): # ⚠️ 直接判断扫描特征,未先检查白名单
|
| 349 |
+
_blacklist_ip(client_ip, reason=f"path={path}")
|
| 350 |
+
...
|
| 351 |
+
```
|
| 352 |
|
| 353 |
**风险**
|
| 354 |
+
- `_is_whitelisted_path()` 函数与 `_WHITELIST_PREFIXES` 常量定义但 `dispatch` 中从未调用
|
| 355 |
+
- `_SCANNER_PATH_PATTERNS` 中包含 `/admin/config`(第 75 行),这是合法的 admin 配置接口路径前缀
|
| 356 |
+
```python
|
| 357 |
+
_SCANNER_PATH_PATTERNS = [
|
| 358 |
+
...
|
| 359 |
+
"/admin/config", # ⚠️ 但 /admin/api/config 是合法 admin 接口
|
| 360 |
+
]
|
| 361 |
+
```
|
| 362 |
+
虽然当前 `/admin/api/config` 不含 `/admin/config` 子串(含,因为是子串匹配 `pat in lower`),实际上 `/admin/api/config` 包含 `/admin/config`?不,`/admin/api/config` 中找 `/admin/config` 子串不存在(`/admin/` 后是 `api/config`)。**但 `/admin/config` 若被合法客户端访问(如旧版前端),会被错误判定为扫描器并拉黑 IP 5 分钟**
|
| 363 |
+
- 类似的还有 `/api/v1/models`(第 71 行)— 这是合法的 OpenAI 兼容客户端常用探测路径,但被当作扫描特征
|
| 364 |
+
- `/metrics`、`/graphql` 等若业务方未来引入,也会被误拦
|
| 365 |
+
- `_is_scanner_path` 用 `pat in lower` 子串匹配,过于宽泛:`.env` 会匹配任何含 `.env` 的路径(如 `/v1/xtc/environment` 假设性路径)
|
| 366 |
+
|
| 367 |
+
**修复建议**
|
| 368 |
+
- 在 `dispatch` 中先调用 `_is_whitelisted_path(path)`,命中白名单则跳过扫描检测
|
| 369 |
+
- `_SCANNER_PATH_PATTERNS` 改为更精确的匹配(如 `$` 结尾锚定,或用正则)
|
| 370 |
+
- 移除 `/admin/config`、`/api/v1/models` 等可能与合法路径冲突的条目
|
| 371 |
+
- 加单元测试覆盖:合法路径不被拦,扫描路径被拦
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
### NM4. 用户注销账号 `_delete_account` 未审计 + Hub 备份可能残留(OWASP API8:2023)
|
| 376 |
|
| 377 |
+
**位置**:`app/api/user_files.py`(基于 summary 引用,未在本次读取的片段中)
|
| 378 |
+
|
| 379 |
+
**风险**
|
| 380 |
+
- 用户注销账号是 GDPR/PIPL 法定的"删除权",需保证彻底删除
|
| 381 |
+
- 当前实现删除 SQLite 行 + Hub 内容,但:
|
| 382 |
+
- 无审计日志记录"谁在何时注销了哪个账号"
|
| 383 |
+
- Hub 上的 `user_accounts/<username>` 备份若异步推送失败可能残留
|
| 384 |
+
- `request_log` 表中该用户的对话历史不会被删除(NH1 的连带问题)
|
| 385 |
+
- `audit_log`、`usage_log`、`session_messages` 等表中该用户的数据不会被清理
|
| 386 |
+
|
| 387 |
+
**修复建议**
|
| 388 |
+
- 注销账号时写审计日志(actor=用户自己,action=user.account.deleted)
|
| 389 |
+
- 注销时同步清理 `request_log`、`session_messages`、`usage_log`、`audit_log` 中该 user_id / access_key 的记录
|
| 390 |
+
- Hub 删除后做二次验证(list 一次确认无残留)
|
| 391 |
+
- 注销前要求二次密码确认(前端已实现,后端应再校验一次)
|
| 392 |
|
| 393 |
---
|
| 394 |
|
| 395 |
+
## 四、Low 级别问题
|
| 396 |
+
|
| 397 |
+
### NL1. `request_log_middleware` 与 `rate_limit_store` 客户端 IP 提取不一致
|
| 398 |
|
| 399 |
+
**位置**
|
| 400 |
+
- `app/request_log_middleware.py:136` `client_ip = request.client.host if request.client else None`(不读 X-Forwarded-For)
|
| 401 |
+
- `app/services/rate_limit_store.py` `get_client_ip()`(读 X-Forwarded-For)
|
| 402 |
+
- `app/scanner_block_middleware.py:98-106` `_get_client_ip()`(读 X-Forwarded-For)
|
| 403 |
|
| 404 |
**风险**
|
| 405 |
+
- 同一请求在不同中间件中记录的 client_ip 不一致
|
| 406 |
+
- request_log 中存的 IP 是反代 IP(如 Cloudflare Worker IP),不是真实客户端 IP,影响基于 IP 的安全分析
|
| 407 |
+
- 鉴权限流用真实 IP,但日志用反代 IP,攻击复盘时无法关联
|
| 408 |
|
| 409 |
+
**修复建议**
|
| 410 |
+
- 统一使用 `rate_limit_store.get_client_ip(request)` 提取客户端 IP
|
| 411 |
+
- 注意 FASTAPI-PROXY-001:仅在受信反代后才信任 X-Forwarded-For
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
---
|
| 414 |
|
| 415 |
+
### NL2. JWT 算法固定 HS256 → 密钥轮换困难(FASTAPI-AUTH-004)
|
| 416 |
|
| 417 |
+
**位置**:`app/auth.py:24` `ALGORITHM = "HS256"`、`app/api/user_files.py:48` 同样
|
| 418 |
|
| 419 |
+
**风险**
|
| 420 |
+
- HS256 单一密钥用于签发与验证,密钥泄露即等于完全失陷
|
| 421 |
+
- 无 `kid` (Key ID) 头部,无法支持多密钥平滑轮换
|
| 422 |
+
- `XTC_JWT_SECRET` 一旦泄露,所有已签发 token 在过期前都有效,无法吊销
|
| 423 |
+
- RS256 / ES256(非对称)允许公钥分发、私钥保护,更适合多服务场景
|
| 424 |
|
| 425 |
+
**修复建议**
|
| 426 |
+
- 短期:保留 HS256,但加 `kid` 头部支持密钥轮换(旧密钥保留用于验证,新密钥用于签发)
|
| 427 |
+
- 长期:迁移到 RS256,私钥仅 admin 持有,公钥可分发到验证方
|
| 428 |
+
- 引入密钥定期轮换机制(如每 90 天)
|
| 429 |
|
| 430 |
+
---
|
| 431 |
|
| 432 |
+
### NL3. `_warn_insecure_secrets()` 仅启动时告警,运行时不阻断
|
| 433 |
|
| 434 |
+
**位置**:`app/main.py` `_warn_insecure_secrets()`(基于 summary)
|
| 435 |
|
| 436 |
+
**风险**
|
| 437 |
+
- 启动告警只在 `lifespan` 启动时打一次,部署者若不查看启动日志(HF Space 默认不展示)则完全无感知
|
| 438 |
+
- 运行时即使检测到弱密钥也不拒绝请求
|
| 439 |
|
| 440 |
+
**修复建议**
|
| 441 |
+
- 启动时若检测到弱密钥 + `ENV=production`,直接 `raise RuntimeError` 拒绝启动
|
| 442 |
+
- 提供 `/health` 接口返回安全状态(仅供受信反代 / 内网访问)
|
| 443 |
+
- 接入 HF Space 的通知机制(如环境变量 `HF_SPACE_AUTHOR_EMAIL` 发邮件)
|
| 444 |
|
| 445 |
+
---
|
| 446 |
|
| 447 |
+
## 五、已修复问题(第一轮,保留作为参考)
|
| 448 |
+
|
| 449 |
+
| ID | 问题 | 状态 |
|
| 450 |
+
|----|------|------|
|
| 451 |
+
| C1 | 通配 CORS `*` | 已修复(`app/cors.py` 白名单模式) |
|
| 452 |
+
| C2 | `/v1/xtc/image/fix/test` SSRF | 已修复(`_fetch_remote_image_safely`) |
|
| 453 |
+
| C3 | `/logs/api/log` 无鉴权 | 已修复(加 `require_access_key`) |
|
| 454 |
+
| C4 | 异常 `str(exc)` 透出客户端 | 已修复(`unhandled_exception_handler` 收敛) |
|
| 455 |
+
| H1 | 鉴权端点无暴力破解防护 | 已修复(`rate_limit_store.check_auth_rate_limit`) |
|
| 456 |
+
| H2 | 限流未覆盖鉴权/日志路径 | 已修复 |
|
| 457 |
+
| H3 | `x-xtc-user-token` 未脱敏 | 已修复 |
|
| 458 |
+
| H4 | 弱密钥无启动告警 | 已修复(`_warn_insecure_secrets`,但见 NL3) |
|
| 459 |
+
| H5 | `.env.example` 缺安全配置 | 已修复 |
|
| 460 |
+
| L1 | `image_fix` 异常透出 | 已修复 |
|
| 461 |
+
| L2 | 图片 URL 无长度限制 | 已修复(8192) |
|
| 462 |
|
| 463 |
---
|
| 464 |
|
| 465 |
+
## 六、本轮修改文件清单
|
| 466 |
+
|
| 467 |
+
本轮为**纯审计报告**,未修改任何源代码文件。修复工作待用户确认后按优先级分批进行。
|
| 468 |
|
| 469 |
+
仅修改 / 新增以下文件:
|
| 470 |
+
- `security_best_practices_report.md`(本报告,覆盖更新)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
---
|
| 473 |
|
| 474 |
+
## 七、修复优先级建议
|
| 475 |
+
|
| 476 |
+
按"风险 × 利用难度"排序,建议按以下顺序修复:
|
| 477 |
+
|
| 478 |
+
1. **立即修复**(1 周内):
|
| 479 |
+
- NC2:未配置 access_key 时强制拒绝启动生产模式
|
| 480 |
+
- NC1:移除 admin `?k=<token>` query 形式;用户 JWT query 形式加 TTL 收紧
|
| 481 |
+
- NC3:admin 数据 API 全部接入 audit_log
|
| 482 |
+
|
| 483 |
+
2. **短期修复**(2 周内):
|
| 484 |
+
- NH1:对话内容入库前脱敏 / 截断
|
| 485 |
+
- NH2:admin login 统一错误响应
|
| 486 |
+
- NH4:备份上传 session 加并发上限
|
| 487 |
+
- NM2:用户 JWT TTL 缩短 + 加服务端吊销
|
| 488 |
|
| 489 |
+
3. **中期修复**(1 个月内):
|
| 490 |
+
- NH3:移除 Basic auth scheme 支持
|
| 491 |
+
- NM1:DB 文件 chmod 600 + 拒绝 /tmp fallback
|
| 492 |
+
- NM3:scanner middleware 修复白名单死代码
|
| 493 |
+
- NM4:注销账号级联清理 + 审计
|
| 494 |
|
| 495 |
+
4. **长期优化**(按规划):
|
| 496 |
+
- NL1/NL2/NL3:IP 提取统一、JWT 算法升级、启动告警升级
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
---
|
| 499 |
|
| 500 |
+
## 八、部署 Checklist(在第一轮基础上新增)
|
| 501 |
+
|
| 502 |
+
生产部署前请逐项确认(在第一轮 checklist 基础上新增):
|
| 503 |
+
|
| 504 |
+
- [ ] `XTC_ACCESS_KEY` 已配置(否则 NC2 风险)
|
| 505 |
+
- [ ] `XTC_ADMIN_ACCESS_TOKEN` 未通过 URL query 共享给他人(NC1)
|
| 506 |
+
- [ ] 用户 JWT TTL 已通过环境变量收紧(若提供 `XTC_USER_TOKEN_TTL_SEC` 配置项)
|
| 507 |
+
- [ ] admin 数据操作审计日志已启用(NC3 修复后)
|
| 508 |
+
- [ ] `/u/api/*` 路径的请求体不被记录到 request_log(NH1 修复后)
|
| 509 |
+
- [ ] DB 文件权限为 600(NM1 修复后)
|
| 510 |
+
- [ ] `/tmp/xtc.db` fallback 路径在生产模式被禁用(NM1 修复后)
|
| 511 |
+
- [ ] 备份上传 session 并发上限已配置(NH4 修复后)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
---
|
| 514 |
|
| 515 |
+
## 九、本轮审计未覆盖范围
|
| 516 |
+
|
| 517 |
+
声明以下范围本轮未深入审查,建议后续补充:
|
| 518 |
+
|
| 519 |
+
- `app/adapters/` 下的 Gemini / OpenAI 适配器代码(请求构造、错误处理)
|
| 520 |
+
- `app/providers/keypool.py` 上游 API Key 轮换逻辑
|
| 521 |
+
- `app/services/webhook_store.py` Webhook URL 是否有 SSRF 校验
|
| 522 |
+
- `app/api/webhooks.py` Webhook 注册接口的 URL 校验
|
| 523 |
+
- `app/api/openai_compat.py` OpenAI 兼容端点的鉴权继承
|
| 524 |
+
- `app/api/sessions.py` 会话 API 的跨用户访问校验(已在第一轮简评,建议本轮补充)
|
| 525 |
+
- 前端 `admin_html.py` / `user_html.py` 内嵌 JS 的 XSS 防护(已用 `esc()` 函数,但未做 CSP 头)
|
| 526 |
+
- HF Space 平台层的网络隔离 / 卷权限(属平台范畴)
|
| 527 |
+
|
| 528 |
+
---
|
| 529 |
|
| 530 |
+
**报告完。** 如需对任一发现展开修复,请告知具体 ID(如 "修复 NC1"),将按"单 finding 单 commit"原则实施。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|