a3216 commited on
Commit
84a4144
·
verified ·
1 Parent(s): 44132c4

sync from GitHub 75f7da8: security: comprehensive audit report (round 2) - 14 new findings

Browse files
Files changed (1) hide show
  1. 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
- - 审计范围:鉴权 / 限流 / CORS / SSRF / 信息泄露 / 输入校验 / 日志脱敏
6
- - 提交基线`cfe155f feat: add package.json for wrangler and update worker.mjs to handle client IP`
 
 
7
 
8
  ---
9
 
10
- ## 一、问题分级汇总
11
 
12
- | 级别 | 数量 | 说明 |
13
- |------|------|------|
14
- | Critical | 4 | 直接导致凭据泄露 / 内网探测 / 任意跨域读取 |
15
- | High | 5 | 暴力破解 / 数据刷屏 / 配置默认值 |
16
- | Low | 2 | 信息暴露面 / 配置档缺失 |
 
 
17
 
18
  ---
19
 
20
- ## 、Critical 级别问题与修复
21
 
22
- ### C1. 通 CORS`Access-Control-Allow-Origin: *`允许任意第三方页面跨域读响应
23
 
24
  **位置**
25
- - `app/main.py` `create_app()` 中 `CORSMiddleware` 配置`allow_origins=["*"]`
26
- - `app/main.py` `validation_error_handler`、`cors_preflight`:硬编码 `*`
27
- - `app/errors.py` `_CORS_HEADERS`、`make_error_response`、`make_upstream_error_response`:硬编码 `*`
28
- - `app/api/_common.py` `CORS_HEADERS`:硬编码 `*`
29
- - `app/middleware.py` `RateLimitMiddleware` 429 响应:硬编码 `*`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
  **风险**
32
- 任意第三方页面(包括恶意站点)发起跨域请求都能读到本服务响应内容,包括错误响应中可能携带的内部信息、上游错误细节、限流信息等。当 `Access-Control-Allow-Credentials` 同时为 `true` 时进一步泄露带凭据请求的响应。
33
-
34
- **修复**
35
- - 新增 `app/cors.py` 作为 CORS 策略中心:
36
- - `get_allowed_origins()` `XTC_CORS_ORIGINS` 读取白名单
37
- - `is_wildcard()`是否为通配模式(未配置白名单)
38
- - `cors_allow_origin(origin)`通配模式回 `*`;白名单模式精确匹配
39
- - `build_cors_headers(origin)`:构造完整 CORS 响应头(白名单模式下未匹配 origin 返回空字典)
40
- - `build_preflight_headers(origin)`:OPTIONS 预检响应头 `Max-Age`
41
- - 新增 `app/config.py` `xtc_cors_origins` 字段 + `_parse_cors_origins` validator(支持逗号/分号/换行分隔字符串或列表)
42
- - `app/main.py` `CORSMiddleware` 根据白名单���态配置:白名单模式 `allow_credentials=True` + 精确 origin;通配模式保持 `*` 向后兼容
43
- - `app/main.py` `validation_error_handler`、`cors_preflight` 改`build_cors_headers(origin)` / `build_preflight_headers(origin)`
44
- - `app/errors.py` `make_error_response`、`make_upstream_error_response` 接受 `origin` 参数并调用 `build_cors_headers`
45
- - `app/api/_common.py` `CORS_HEADERS` 改为 `build_cors_headers(None)` 计算结果;`ok_json`/`ok_with_cors` 新增 `origin` 可选参数
46
- - `app/middleware.py` 429 响应改用 `build_cors_headers(request.headers.get("origin"))`
47
-
48
- **配置示例**
49
- ```
50
- XTC_CORS_ORIGINS=https://your-app.example.com,https://admin.example.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ```
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ---
54
 
55
- ### C2. SSRF:`/v1/xtc/image/fix/test` 任意 URL 拉取未做协议/内网校验
56
 
57
- **位置**:`app/api/image_fix.py` `fix_test()` 中 `httpx.AsyncClient().get(image_url)`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  **风险**
60
- 攻击者可传入
61
- - `http://169.254.169.254/latest/meta-data/` 探测云元数据(AWS/Azure/GCP 实例凭据
62
- - `http://localhost:port/` 访问本机内网服务(Redis/admin 接口/数据库等
63
- - `http://10.0.0.1/`、`http://192.168.x.x/` 扫描内网
64
- - `file:///etc/passwd``gopher://...` 等危险协议
65
- - 重定向到上述目标绕过校验
66
-
67
- **修复**:新增 `_fetch_remote_image_safely(url)`
68
- - 协议白名单:只允许 `http`/`https`
69
- - 私网 IP 拦截:`_is_private_ip(host)` IP 字面量与域名解析结果,拒绝私网/回环/链路本地/保留/组播/未指定地址
70
- - 重定向重新校验:`follow_redirects=False`,手动跟随,每次重定向目标都重新走 SSRF 校验
71
- - 重定向循环检测:`seen_urls` 集合
72
- - 重定向上限:3 次
73
- - 大小限制:`Content-Length` 与实际 `len(raw)` 双校验,上限 20MB(防内存爆破)
74
- - 输入长度限制:`body.image` 长度上限 8192
75
- - 错误信息收敛:`fix_image_bytes` 异常不透出 `str(exc)`,只回固定文案 + 服务端 `logger.exception`
76
 
77
  ---
78
 
79
- ### C3. `/logs/api/log` 与 `/logs/api/log/batch` 无鉴权,任意人可写入垃圾日志
80
 
81
- **位置**:`app/api/logs.py` `post_log`、`post_log_batch`
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  **风险**
84
- - 任意未鉴权客 `app_logs` 表写入无限日志,撑爆 SQLite 数据库
85
- - 字段无长度限制单条日志可写 MB 数据
86
- - 批量接口无条数上限,一次请求写入任意条数
87
- - 攻击者借此进行 DoS(数据库写入是同步的会阻塞事件循环)
88
 
89
- **修复**
90
- - 两个端点加 `Depends(require_access_key)` 鉴权
91
- - 字段长度限制:`level ≤ 32`、`module ≤ 128`、`event ≤ 128`、`trace_id 128``data ≤ 64KB`
92
- - 批量条数上限:`_MAX_BATCH_SIZE = 200`超出截断
93
- - `rate_limit_store.is_rate_limited_path()` 把 `/logs/api/log`、`/logs/api/log/batch` 纳入限流
94
 
95
  ---
96
 
97
- ### C4. `unhandled_exception_handler` `str(exc)` 返回给客户端
98
-
99
- **位置**:`app/errors.py` `unhandled_exception_handler`
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  **风险**
102
- 未捕获异常的 `str(exc)` 可能包含
103
- - 内部文件路径(`/workspace/xtc-backend-hf/app/...`
104
- - SQL 片段(`sqlite3.OperationalError: no such table: xxx`)
105
- - 密钥片段(`KeyError: 'hf_token'`
106
- - 第三方库版本与端点信息(`httpx.ConnectError: ...`)
107
 
108
- 攻击者通过构造畸形请求触发异常,可获取内部现细节用于后续攻击。
109
 
110
- **修复**
111
- - 客户端只返回固定文案 `"Unexpected error"`,不暴露 `str(exc)`
112
- - 完整 `traceback` 只打到服务端日志(`logging.getLogger().error` + `traceback.print_exc`)
113
- - 异常处理函数接受 `request`,提取 `request.headers.get("origin")` 走动态 CORS
114
 
115
  ---
116
 
117
- ## 三、High 级别问题与修复
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
- ### H1. 鉴权类端点无暴力破解防护
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- **位置**:`/u/api/login`、`/u/api/register`、`/admin/api/login`
 
 
 
 
 
 
 
 
 
 
 
122
 
123
  **风险**
124
- - 没有针对 IP / 用户名的失败计数与锁定
125
- - 没有比业务接口更严格的限流
126
- - 攻击者可对单一账号无限尝试密码或对后台 admin key 进行暴力破解
127
-
128
- **修复**:`app/services/rate_limit_store.py` 新增鉴权限流模块
129
- - `_AUTH_WINDOW_SEC = 60`、`_DEFAULT_AUTH_PER_IP = 10`、`_DEFAULT_AUTH_PER_USERNAME = 5`
130
- - `_AUTH_FAIL_THRESHOLD = 5`、`_AUTH_FAIL_LOCK_SEC = 15 * 60`
131
- - 三层防护
132
- 1. **失败锁定**:`is_ip_locked(ip)` 检查该 IP 15 分钟失败数,≥5 次直接 429 拒绝
133
- 2. **per-IP 限流**:`check_auth_rate_limit()` IP 60 秒最多 10鉴权请求
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
- ### H2. `is_rate_limited_path` 未把鉴权路径与日志写入端点纳入限流
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- **位置**:`app/services/rate_limit_store.py`
 
 
 
 
149
 
150
- **修复**
151
- - 加入 `/u/api/login`、`/u/api/register`、`/admin/api/login` 到鉴权流路径
152
- - 加入 `/logs/api/log``/logs/api/log/batch` 到限流路径
153
- - 新增 `is_auth_path(path)` 用于区分鉴权限流与业务限流
 
154
 
155
  ---
156
 
157
- ### H3. 敏感标头脱敏不完整:`x-xtc-user-token` 未脱敏
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- **位置**:`app/services/request_log_store.py` `_SENSITIVE_HEADERS`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  **风险**
162
- 用户登录后的 JWT 令牌通过 `x-xtc-user-token` 头携带,未脱敏会写入 `request_log` 表,管理员查看请求日志时可读到完整 token,存在凭据泄露面。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- **修复**:`_SENSITIVE_HEADERS` 增加 `x-xtc-user-token`;`_SENSITIVE_BODY_KEYS` 增加 `user_token`、`admin_access_token`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  ---
167
 
168
- ### H4. 无启动期弱密钥/默认密钥告警
 
 
169
 
170
- **位置**:`app/main.py` `lifespan`
 
 
 
171
 
172
  **风险**
173
- 默认值 `change-me-admin-key` / `change-me-access-key` / `change-me-jwt-secret-strong-random` 容易被遗忘部署到生产环境,且没有提示。
 
 
174
 
175
- **修复**:`app/main.py` 新增 `_warn_insecure_secrets()`,启动时检查并 `logging.warning`:
176
- - 密钥为空
177
- - 密钥仍为默认值
178
- - 密钥长度 < 16
179
- - CORS 为通配模式
180
- - Admin 页面无门禁 token
181
 
182
  ---
183
 
184
- ### H5. `.env.example` 缺少安全配置项与告警
185
 
186
- **位置**:`.env.example`
187
 
188
- **修复**
189
- - 顶部新增"安全提示"块,列出生产部署前必须检查的 5 项
190
- - 新增 `XTC_CORS_ORIGINS=` 配置项及说明
 
 
191
 
192
- ---
 
 
 
193
 
194
- ## 四、Low 级别问题与修复
195
 
196
- ### L1. `image_fix` 拉取失败时向客户端透出 `str(e)`
197
 
198
- **位置**:`app/api/image_fix.py`
199
 
200
- **修复**:`httpx.HTTPError` 只回固定文案 `"failed to fetch image"`;`fix_image_bytes` 异常只回 `"image fix failed"` + 服务端 `logger.exception`
 
 
201
 
202
- ### L2. 图片 URL 无长度限制
 
 
 
203
 
204
- **位置**:`app/api/image_fix.py`
205
 
206
- **修复**:`body.image` 长度上限 8192超出 400 拒绝
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  ---
209
 
210
- ## 已存在的安全机制(继���保留)
 
 
211
 
212
- - bcrypt 密码哈希(`_hash_password` / `_verify_password`)
213
- - JWT HS256 签名 + SQLite 吊销列表(`verify_access_token`)
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
- ## 部署 checklist
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
- 部署到生产前请逐项确认
 
 
 
 
227
 
228
- - [ ] `XTC_ADMIN_KEY` 已改为 ≥32 字符随机串`openssl rand -hex 32`
229
- - [ ] `XTC_ACCESS_KEY` 已改为 ≥32 字符随机串
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
- | `app/cors.py` | 新增 | CORS 策略中心 |
246
- | `app/config.py` | 修改 | 新增 `xtc_cors_origins` 字段与 validator |
247
- | `app/main.py` | 修改 | 启动告警 / 动态 CORSMiddleware / 动态预检 |
248
- | `app/errors.py` | 修改 | 动态 CORS / `unhandled_exception_handler` 不暴露 `str(exc)` |
249
- | `app/api/_common.py` | 修改 | 动态 CORS_HEADERS / `ok_json` 支持 `origin` 参数 |
250
- | `app/api/image_fix.py` | 修改 | SSRF 防护 / 输入长度 / 异常信息收敛 |
251
- | `app/api/logs.py` | 修改 | access_key 鉴权 / 字段长度限制 / 批量上限 |
252
- | `app/middleware.py` | 修改 | 鉴权流接入 / 429 动态 CORS |
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
- - 引入结构化日志JSON方便接入 SIEM
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"原则实施。