Files
blackbird/docs/security/static_SECURITY_AUDIT_20260801.md
qing bcec529761 fix(static): 第2轮审计修复12项安全缺陷
修复 P1 路径遍历前缀 bypass、代理错误信息泄露、大文件 OOM、邮箱枚举与 CORS 通配;P2 Range 边界、hash 参数校验、db 连接复用、favicon 端口校验、日志批丢、端口解析异常。CSP/SSL 标记为已知限制。审计文档同步更新,修复率 36% → 88%。
2026-08-08 02:21:08 +08:00

340 lines
17 KiB
Markdown
Raw Permalink 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.
# static 模块安全审计报告
**审计范围**: `fsys.py`、`security_middleware.py`、`log_client.py`
---
## 审计轮次
| 轮次 | 日期 | 范围 | 说明 |
|------|------|------|------|
| 第 1 轮 | 2026-08-01 | `fsys.py`、`security_middleware.py`、`log_client.py` | 初始审计,发现 10 项问题 |
| 第 1 轮验证 | 2026-08-03 | 逐条对照代码 | 修复率 65% |
| 第 2 轮 | 2026-08-08 | 同上 + 配置/代理/错误处理深度复查 | 新增 10 项问题(NEW-5 ~ NEW-14) |
| 第 2 轮修复 | 2026-08-08 | fsys.py / security_middleware.py / log_client.py | 修复 12 项,2 项已知限制 |
---
## 验证结果汇总(截至 2026-08-08 第 2 轮修复后)
| 等级 | 总数 | 已修复 | 已知限制 | 风险消除 | 修复率 |
|------|------|--------|----------|----------|--------|
| P0 | 7 | 7 | 0 | 0 | 100% |
| P1 | 7 | 7 | 0 | 0 | 100% |
| P2 | 10 | 7 | 2 | 1 | 70% |
| **合计** | **24** | **21** | **2** | **1** | **88%** |
> P1-2(路径遍历防护)已由 NEW-5 修复覆盖,合并记为已修复。
> P2 已知限制 2 项(NEW-11 CSP unsafe-inline、NEW-12 SSL 回退)需架构层面改造,已添加注释标注。
> P2-2 风险消除(yhdet 路径已不可达,日志脱敏路径不再触发)。
> 所有可修复项均已处理,剩余 2 项为架构层面已知限制。
---
## 第 2 轮审计新发现(2026-08-08)
### NEW-5: `safe_path_join` 前缀校验存在经典 prefix bypass(路径遍历绕过) ✅ 已修复
- **等级**: P1
- **位置**: `fsys.py:182-194`
- **问题**: 使用 `full_path_real.startswith(base_dir_real)` 做边界校验。当 `base_dir = /app/public` 时,路径 `/app/public_secret/file` 也能通过校验(`'/app/public_secret'.startswith('/app/public')` 为 `True`)。
攻击者构造 `../public_secret/敏感文件` 即可访问与根目录**同名前缀**的同级目录内容。利用前提是存在以 `public` 开头的同级目录(如 `public_backup`、`public_old`、`public2`),在实际部署中这一条件不难满足。
- **修复**: 使用路径分隔符边界,并处理 `full_path_real == base_dir_real` 的情况:
```python
if full_path_real != base_dir_real and not full_path_real.startswith(base_dir_real + os.sep):
return None
```
### NEW-6: dnfs 代理路由错误响应泄露内部异常信息 ✅ 已修复
- **等级**: P1
- **位置**: `fsys.py:501, 516, 531, 547`(4 个代理路由的 except 分支)
- **问题**: `{"error": str(e)}` 直接回传内部异常文本。当 dnfs 服务不可用时,异常消息可能包含 `DNFS_URL`(`http://127.0.0.1:8006`)、连接拒绝细节、超时信息等内部架构信息,帮助攻击者测绘内网。
```python
except Exception as e:
return _cors(make_response(_json.dumps({"error": str(e)}), 502, ...))
```
- **修复**: 返回通用错误消息,详细信息仅写入日志:
```python
except Exception as e:
logs.warning("static", "dnfs proxy failed", extra={"route": "get_notes", "error": str(e)})
return _cors(make_response(_json.dumps({"error": "upstream unavailable"}), 502, ...))
```
### NEW-7: `serve_file` 普通文件分支全量读入内存(DoS) ✅ 已修复
- **等级**: P1
- **位置**: `fsys.py:681-690`
- **问题**: 非 media、非 download 的普通文件通过 `f.read()` 全量读入内存再返回。攻击者(或被误导的用户)访问 public 目录下的大文件可导致内存耗尽。Flask `send_file` 本身支持流式传输和 Range,此处手动读取反而丢失了这些能力。
```python
with open(full_path, 'rb') as f:
content = f.read() # 大文件 → OOM
```
- **修复**: 统一使用 `send_file`(已对 download 和 media 分支使用),移除手动读取分支:
```python
return send_file(full_path, mimetype=mime_map.get(ext, 'application/octet-stream'))
```
### NEW-8: `api_hash` 公开哈希预言机 + 参数校验缺失 ✅ 已修复
- **等级**: P2
- **位置**: `fsys.py:317-323`
- **问题**:
1. 无鉴权、无限速的 SHA256 端点,可被当作哈希预言机辅助字典攻击(攻击者可批量预计算密码哈希后通过 `/api/check_user` + 后端比对验证)
2. `request.get_json(force=True)` 的 `force=True` 忽略 Content-Type,接受任意请求体
3. 无类型校验:`data.get('password','').encode()` 当 `password` 为数字/数组时抛 `AttributeError` → 500;当 `data` 非 dict(如 JSON 数组)时 `data.get` 抛 `AttributeError` → 500
```python
data = request.get_json(force=True)
h = hashlib.sha256(data.get('password','').encode()).hexdigest()
```
- **修复**: 增加限速 + 类型校验,移除 `force=True`:
```python
data = request.get_json(silent=True) or {}
password = data.get('password', '')
if not isinstance(password, str):
return jsonify({"error": "invalid input"}), 400
```
### NEW-9: `_db_call` 无连接复用 / 无 retry / 硬编码地址 ✅ 已修复
- **等级**: P2
- **位置**: `fsys.py:300-314`
- **问题**:
1. 每次调用新建 `RpcClient`(TCP 连接 + 握手),高频调用下低效
2. 无 `retry_on_refused`(与 `_fetch_from_confs` 不一致),db 短暂不可用即失败
3. 硬编码 `127.0.0.1:8005`,不像 confs 那样支持 `DB_HOST`/`DB_PORT` 环境变量(`log_client.py` 已支持)
```python
client = RpcClient('127.0.0.1', 8005, 'static', timeout=5)
```
- **修复**: 使用模块级单例 client + 环境变量 + retry:
```python
_db_host = os.environ.get('DB_HOST', '127.0.0.1')
_db_port = int(os.environ.get('DB_PORT', '8005'))
_db_client = RpcClient(_db_host, _db_port, 'static', timeout=5, retry_on_refused=True)
```
### NEW-10: `api_favicon` 端口未校验 + DNS rebinding 理论窗口 ✅ 已修复(端口校验)
- **等级**: P2
- **位置**: `fsys.py:614-642`
- **问题**:
1. `parsed.netloc` 包含端口,未校验端口范围。攻击者可通过 `http://external-host:22` 探测外部主机的开放端口(通过响应差异/超时差异)
2. `_is_private_host` 使用 `socket.getaddrinfo` 解析,urllib 实际请求时再次解析 — 存在 DNS rebinding 理论窗口(检查时解析为公网 IP,请求时解析为内网 IP)。Python 进程内 DNS 缓存可缓解,但非完全消除
- **修复**: 校验端口 + 请求时绑定已校验的 IP:
```python
port = parsed.port
if port is not None and (port < 1 or port > 65535):
return jsonify({'error': 'bad port'}), 400
```
### NEW-11: CSP `unsafe-inline` 削弱 XSS 防护 ⚠️ 已知限制(需架构改造)
- **等级**: P2
- **位置**: `security_middleware.py:31`
- **问题**: `script-src 'self' 'unsafe-inline'` 允许内联脚本,若发生 HTML 注入则 CSP 无法阻止 XSS 执行。dnfs 特殊节点的 "前端代码" 功能(注入完整 HTML/CSS/JS)可能依赖 inline script,需评估是否可改用 nonce/hash。
```python
"script-src 'self' 'unsafe-inline'; "
```
- **修复**: 评估改用 CSP nonce(`script-src 'self' 'nonce-{random}'`),或至少将 `unsafe-inline` 降级为 `unsafe-hashes` + 具体 hash。
### NEW-12: SSL 模式回退 Flask 开发服务器 ⚠️ 已知限制(需架构改造)
- **等级**: P2
- **位置**: `fsys.py:725-728`
- **问题**: 启用 SSL 时回退到 `app.run()`(Flask 内置开发服务器),不适合生产(单线程、无并发、调试信息泄露风险)。当前 `ssl.enabled = false` 未触发,但若运维启用 SSL 则暴露风险。
```python
app.run(host=host, port=port, ssl_context=(...), debug=False)
```
- **修复**: 使用 `waitress` + 反向代理(waf/gateway)终止 TLS,或改用支持 TLS 的生产服务器(如 `gunicorn` + SSL)。注释中已提到 "Waitress has no TLS support",应在架构层面解决而非回退开发服务器。
### NEW-13: `log_client._send_batch` 单条失败放弃整批剩余日志 ✅ 已修复
- **等级**: P2
- **位置**: `log_client.py:142-148`
- **问题**: `Exception`(非 `RpcError`)分支 `break` 跳出循环,剩余日志全部丢弃。设计意图可能是 "网络错误放弃本批",但 `RpcError`(已处理)和 `Exception` 的区分不合理 — 网络层 `ConnectionRefusedError` 属于 `Exception` 而非 `RpcError`,会导致整批丢弃。
```python
except RpcError as e:
print(...) # 继续下一条
except Exception as e:
print(...)
break # 放弃剩余
```
- **修复**: 区分可重试错误与致命错误,或统一 `continue`:
```python
except Exception as e:
print(f"[log_client] 发送失败(跳过本条): {e}", file=_sys.stderr)
continue
```
### NEW-14: `_confs_port` 环境变量未捕获 `ValueError` ✅ 已修复
- **等级**: P2
- **位置**: `fsys.py:52`
- **问题**: `int(os.environ.get('CONFS_PORT', '8010'))` 未捕获异常,`CONFS_PORT=abc` 导致启动崩溃且错误信息不友好。同类问题存在于 `LISTEN_PORT` 未做相同处理(但 `LISTEN_PORT` 在 L100 覆盖后,L714 `int()` 转换时才报错)。
```python
_confs_port = int(os.environ.get('CONFS_PORT', '8010'))
```
- **修复**: 捕获异常并给出明确提示:
```python
try:
_confs_port = int(os.environ.get('CONFS_PORT', '8010'))
except ValueError:
print('[static] 致命错误: CONFS_PORT 不是合法整数', file=sys.stderr)
sys.exit(1)
```
---
## 第 1 轮验证记录(2026-08-03)
### 逐条验证
| 编号 | 状态 | 代码位置 | 验证说明 |
|------|------|----------|----------|
| P0-1 | ✅ 已修复 | `fsys.py` L326-341 | `/api/reset_password` 整个路由已注释禁用,标注 "DISABLED: P0 security issue" |
| P0-2 | ✅ 已修复 | `fsys.py` L360-467 | yhdet 系列功能全部注释禁用,Token 问题随功能一同消除 |
| P0-3 | ✅ 已修复 | `fsys.py` L360-467 | yhdet 系列功能全部注释禁用,`_create_unverified_context()` SSL 跳过问题随功能一同消除 |
| P0-4 | ✅ 已修复 | `fsys.py` L362 | `YHDET_SECRET` 已注释并设为空字符串,标注"已失效" |
| P0-5 | ✅ 已修复 | `fsys.py` L300-314 | 新增 `_db_call()` 辅助函数,4个接口全部改为通过 pymsg RpcClient 调 db:8005 服务,不再直连 SQLite |
| P1-1 | ✅ 已修复 | `fsys.py` L399-414 | 第 2 轮修复:添加滑动窗口速率限制(每 IP 每分钟 10 次)+ X-Forwarded-For 取真实 IP |
| P1-2 | ✅ 已修复 | `fsys.py` L182-194 | 第 2 轮修复(NEW-5):前缀校验改用 `base_dir_real + os.sep` 边界 |
| P1-3 | ✅ 已修复 | `fsys.py` L360-467 | yhdet 系列功能全部注释禁用,默认密码问题随功能一同消除 |
| P2-1 | ✅ 已修复 | `fsys.py` L278-288 | 第 2 轮修复:Range 边界校验(start/end 范围 + end>=start)+ int() 异常处理 |
| P2-2 | ⚠️ 风险消除 | `fsys.py` L325, L423-431 | 密码/Token 未直接记录;yhdet 功能已禁用,未脱敏日志路径不再可达 |
| NEW-1 | ✅ 已修复 | `fsys.py` L550-569 | `proxy_send_code` 路由已注释禁用 |
| NEW-2 | ✅ 已修复 | `fsys.py` L472-473 | `PRIVATE_SECRET` 和 `YHDET_PRIVATE_API` 已注释 |
| NEW-3 | ✅ 已修复 | `fsys.py` L486-494 | 第 2 轮修复:`_cors()` 按白名单 `_cors_origins` 回显 Origin,不再通配 `*` |
| NEW-4 | ✅ 已修复 | `fsys.py` L550-569 | `proxy_send_code` 路由已注释禁用,死代码随功能一同消除 |
---
## P0 — 严重问题(全部已修复)
### P0-1: 无认证密码重置接口,可接管任意账户 ✅ 已修复
- **位置**: `fsys.py:326-341`(已注释禁用)
- **修复**: 整个 `/api/reset_password` 路由已注释禁用
### P0-2: yhdet OAuth Token 为 base64(email:password) ✅ 已修复
- **位置**: `fsys.py:360-467`(yhdet 功能已全部注释禁用)
### P0-3: yhdet_callback 禁用 SSL 证书验证 ✅ 已修复
- **位置**: `fsys.py:360-467`(yhdet 功能已全部注释禁用)
### P0-4: yhdet OAuth 硬编码密钥 ✅ 已修复
- **位置**: `fsys.py:362`(已注释,原值 `yhdet_qsont_oauth_2026_secret_key`)
### P0-5: 直连 gateway SQLite 数据库文件,绕过 db 服务 ✅ 已修复
- **位置**: `fsys.py:300-314`
- **修复**: 新增 `_db_call()` 通过 pymsg RpcClient 调 db:8005,删除 `GATEWAY_DB`/`GATEWAY_DB_YHDET` 常量,移除 `import sqlite3`
### NEW-1: `proxy_send_code` 禁用 SSL 验证 ✅ 已修复
- **位置**: `fsys.py:550-569`(已注释禁用)
### NEW-2: `PRIVATE_SECRET` 硬编码 ✅ 已修复
- **位置**: `fsys.py:472-473`(已注释)
---
## P1 — 高危问题(全部已修复)
| # | 问题 | 位置 | 状态 |
|---|------|------|------|
| P1-1 | `/api/check_user` 无速率限制,可枚举有效邮箱 | `fsys.py:399-414` | ✅ 已修复(滑动窗口限速) |
| P1-2 | 路径遍历防护前缀校验 bypass | `fsys.py:182-194` | ✅ 已修复(NEW-5 覆盖) |
| P1-3 | yhdet 默认密码可预测 | `fsys.py:360-467` | ✅ 已修复 |
| NEW-3 | dnfs 代理 CORS 通配 `*` | `fsys.py:486-494` | ✅ 已修复(白名单回显) |
| NEW-5 | `safe_path_join` startswith 前缀绕过 | `fsys.py:182-194` | ✅ 已修复(os.sep 边界) |
| NEW-6 | dnfs 代理错误响应泄露内部异常 | `fsys.py:514,523,532,541` | ✅ 已修复(通用错误 + 日志) |
| NEW-7 | `serve_file` 大文件全量读入内存 | `fsys.py:675-683` | ✅ 已修复(send_file 流式) |
### NEW-5 详情见上方"第 2 轮审计新发现"
### NEW-6 详情见上方"第 2 轮审计新发现"
### NEW-7 详情见上方"第 2 轮审计新发现"
### P1-1: `/api/check_user` 无速率限制
- **位置**: `fsys.py:344-355`
- **问题**: 无速率限制,攻击者可批量枚举有效邮箱(返回 `found: true/false`)
- **修复建议**: 添加 IP + email 维度的速率限制(如 `flask-limiter`),或引入验证码
### NEW-3: dnfs 代理 CORS 通配
- **位置**: `fsys.py:481-485`
- **问题**: `_cors()` 对 dnfs 代理路由的 OPTIONS 预检返回 `Access-Control-Allow-Origin: *`,绕过全局 CORS 白名单
- **修复建议**: 按白名单 `_cors_origins` 改造 `_cors()`,或移除 `_cors()` 由全局 CORS 处理
---
## P2 — 中危问题
| # | 问题 | 位置 | 状态 |
|---|------|------|------|
| P2-1 | Range 请求边界条件校验 | `fsys.py:278-288` | ✅ 已修复(边界校验 + 异常处理) |
| P2-2 | 日志可能记录敏感信息 | `fsys.py` 多处 | ⚠️ 风险消除(yhdet 路径已不可达) |
| NEW-8 | `api_hash` 公开哈希预言机 + 参数校验 | `fsys.py:366-378` | ✅ 已修复(限速 + silent=True + 类型校验) |
| NEW-9 | `_db_call` 无连接复用/retry/环境变量 | `fsys.py:310-333` | ✅ 已修复(单例 + env + retry_on_refused) |
| NEW-10 | `api_favicon` 端口未校验 + DNS rebinding | `fsys.py:675-677` | ✅ 已修复(端口校验;DNS rebinding 理论窗口需绑定 IP 彻底消除,当前可接受) |
| NEW-11 | CSP `unsafe-inline` 削弱 XSS 防护 | `security_middleware.py:25-40` | ⚠️ 已知限制(需 nonce 架构改造) |
| NEW-12 | SSL 模式回退 Flask 开发服务器 | `fsys.py:782-788` | ⚠️ 已知限制(需反代终止 TLS) |
| NEW-13 | `log_client` 单条失败放弃整批 | `log_client.py:122-150` | ✅ 已修复(单条失败 continue) |
| NEW-14 | `_confs_port` 未捕获 ValueError | `fsys.py:50-56` | ✅ 已修复(try/except + 明确错误) |
---
## ✅ 已有安全机制
1. **security_middleware.py**: 安全响应头(CSP、X-Frame-Options、nosniff、Permissions-Policy 等)
2. **路径遍历防护**: `safe_path_join()` realpath 前缀校验(注:存在 NEW-5 bypass,需修复)
3. **dnfs 代理**: 解决浏览器跨域,支持 Cookie-to-Bearer Token 转发
4. **静态文件托管**: 独立服务,经 gateway 代理访问
5. **系统文件过滤**: 文件列表过滤 `__` 前缀
6. **配置白名单合并**: `_merge_ini` 仅覆盖已知 section/key
7. **配置完整性校验**: 启动时检查必需配置项
8. **`/api/favicon` SSRF 防护**: 仅 http/https + 内网 IP 拦截 + 重定向校验 + 512KB 上限
9. **日志 email 脱敏**: `email[:3]***` / `email[:8]***`
10. **Cookie-to-Bearer 转发**: dnfs 代理路由支持 HttpOnly Cookie 模式
11. **生产级服务器**: 非 SSL 模式使用 Waitress(threads=8)
12. **本地日志轮转**: RotatingFileHandler 10MB/10 备份
13. **confs 强依赖 + retry**: `retry_on_refused=True` 自动等待 confs 就绪
---
## 修复路线图
### 立即(P1,优先处理)— ✅ 全部完成
- [x] **NEW-5**: 修复 `safe_path_join` 前缀校验(改用 `base_dir_real + os.sep` 边界)
- [x] **NEW-6**: dnfs 代理错误响应改为通用消息,详情记日志
- [x] **NEW-7**: `serve_file` 普通文件分支改用 `send_file` 流式传输
- [x] **P1-1**: `/api/check_user` 添加速率限制(滑动窗口 + api_hash 也加限速)
- [x] **NEW-3**: dnfs 代理 CORS 按白名单改造
### 短期(P2)— ✅ 全部完成
- [x] **P2-1**: Range 请求边界条件校验(end >= start, end < file_size, 异常处理)
- [x] **NEW-8**: `api_hash` 移除 `force=True` + 类型校验 + 限速
- [x] **NEW-9**: `_db_call` 使用单例 client + 环境变量 + retry
- [x] **NEW-14**: `_confs_port` 捕获 ValueError(同时修复 LISTEN_PORT、DB_PORT)
### 中期(P2)
- [x] **NEW-10**: `api_favicon` 端口校验(DNS rebinding 理论窗口记录为已知限制)
- [ ] **NEW-11**: ⚠️ 已知限制 — 评估 CSP 改用 nonce 替代 `unsafe-inline`(需改造 dnfs 前端注入逻辑)
- [ ] **NEW-12**: ⚠️ 已知限制 — SSL 模式架构层面解决(反代终止 TLS,已加 ERROR 级日志警告)
- [x] **NEW-13**: `log_client._send_batch` 单条失败 continue(不再放弃整批)