# 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(不再放弃整批)