Merge pull request 'Dev qing' (#20) from dev_qing into main
Reviewed-on: qing/micro_servers#20
This commit is contained in:
commit
8b3863f3ae
4 files changed
+33
-41
No files matched your search
+23
-29
@@ -45,7 +45,7 @@ Dendrachy Network File-System,基于父子关系(pid)组织的树状节点
|
||||
Authorization: Bearer <base64_payload>_<rsa_sign>
|
||||
```
|
||||
|
||||
- Token 由 gateway 在 `/login` 签发,dnfs 通过 [dnfs.py:304](dnfs.py#L304) `decode_token` 仅解 Base64 取 payload,**当前代码不验签**(代码中标注 `# FIXME: 验证数字签名`,依赖部署上经 gateway 转发的网络隔离)。
|
||||
- Token 由 gateway 在 `/login` 签发;dnfs 通过 [dnfs.py:312](dnfs.py#L312) `decode_token` 将 Token 转发至 gateway `/whoami` 接口,由 gateway 完成 **RSA 签名验证 + 过期检查**(不再本地解码验签);gateway 不可达时按 **fail-closed** 返回 503,验签失败返回 None。
|
||||
- payload 中取 `user` 字段作为操作者,写入 `owner`。
|
||||
- `delete_note` 与 `update_note` 校验所有权:
|
||||
- 节点 `owner` 必须等于当前 `user`,否则 `403`;
|
||||
@@ -306,45 +306,39 @@ bash _run.sh # 启动服务(waitress)
|
||||
|
||||
## 已知问题与风险
|
||||
|
||||
审计日期: 2026-08-01
|
||||
> 状态基准:2026-08-01 审计 + 2026-08-03 逐条代码验证。
|
||||
|
||||
### 🔴 P0 — 严重问题(需立即修复)
|
||||
### 🔴 P0 — 严重问题
|
||||
|
||||
1. **Token 无 RSA 签名验证,可伪造任意用户身份(含管理员)** — [dnfs.py:304-311](dnfs.py#L304-L311)
|
||||
- `decode_token()` 直接 base64 解码 payload,完全跳过 RSA 签名验证(代码中有 `# FIXME: 验证数字签名` 注释)
|
||||
- 若 dnfs 被直接访问(绕过 gateway),攻击者可构造 `{"user": "任意邮箱", "is_admin": true}` 的 payload,base64 编码后拼接下划线+任意字符串即可伪造管理员身份,绕过所有权限校验
|
||||
- **参考实现**: gateway `verifyWsAdminToken` 展示了正确流程:分割 `_` → base64 解码 → RSA 公钥验签 → 检查过期时间
|
||||
- **修复建议**: 实现 RSA 签名验证(持有 gateway 公钥)+ Token 过期校验(`iat`/`created_at` + `keep_time`)
|
||||
1. **Token 无 RSA 签名验证,可伪造任意用户身份(含管理员)** — ✅ 已修复
|
||||
- 原 `decode_token()` 本地 base64 解码 payload、跳过 RSA 验签(`# FIXME: 验证数字签名`)。
|
||||
- 现已重写为转发 gateway `/whoami` 完成验签 + 过期检查([dnfs.py:312-351](dnfs.py#L312-L351)),gateway 不可达时 fail-closed 返回 503。
|
||||
|
||||
2. **`review_content()` 错误处理断裂,fail-closed 失效** — [dnfs.py:149-195](dnfs.py#L149-L195)
|
||||
- 成功路径返回 `bool`,但 HTTPError/URLError/其他异常直接 `raise e`,没有返回 `False` 实现 fail-closed
|
||||
- 内层 `except json.JSONDecodeError as e` 变量名遮蔽外层 `HTTPError as e`
|
||||
- ai_review 不可用时异常向上传播导致 500,而非拒绝内容(违反 fail-closed 设计意图)
|
||||
- **修复建议**: 所有异常路径返回 `False` 或抛 DnfsExp 拒绝写入
|
||||
2. **`review_content()` 错误处理 / fail-closed** — ⚠️ 基本修复
|
||||
- 已实现 fail-closed:HTTPError/URLError/JSONDecodeError 等异常路径均拒绝请求(写入/更新被拒、负载不送审)。
|
||||
- 遗留:函数返回类型不一致(成功返回 `bool`,而 `_validate_sp` 等期望 dict),建议代码清理(审计 P2-1)。
|
||||
|
||||
3. **`delete_note` 和 `update_note` 缺少 `@_handle_exp` 装饰器** — [dnfs.py:428](dnfs.py#L428), [dnfs.py:531](dnfs.py#L531)
|
||||
- `add_note`/`get_notes` 使用了 `@_handle_exp` 统一异常处理,但 `delete_note`/`update_note` 没有
|
||||
- 抛出 `DnfsExp` 或其他异常时返回 Flask 默认 500 HTML 错误页,与其他接口 JSON 响应不一致
|
||||
3. **`delete_note` / `update_note` 缺少 `@_handle_exp` 装饰器** — ✅ 已修复
|
||||
- 现两函数均已加 `@_handle_exp`([dnfs.py:469](dnfs.py#L469)、[dnfs.py:572](dnfs.py#L572)),异常统一返回结构化 JSON,不再出 Flask 默认 500 页。
|
||||
|
||||
4. **有子节点的父节点可能被删除(树结构完整性保护缺陷)**
|
||||
- **数据库层缺失外键约束**: 虽然 dbv2 启用了 `PRAGMA foreign_keys=ON`,但 Notes 表建表 SQL([db/create_sqlite3.sql:13-19](../db/create_sqlite3.sql#L13-L19))中 `pid` 列只是普通 INTEGER,没有定义 `FOREIGN KEY (pid) REFERENCES Notes(id) ON DELETE RESTRICT`
|
||||
- **应用层 TOCTOU 竞态**: 子节点查询(SELECT)和删除(DELETE)是两个独立 RPC 调用,无事务保护,检查与删除之间可插入子节点导致父节点被删后子节点成为孤儿
|
||||
- **错误处理缺陷**: `_call_dbv2` 返回 `success=False` 时响应中无 `data` 键,直接访问 `notes['data']` 会抛 KeyError 而非正确处理
|
||||
- **修复建议**: (a) 添加数据库外键约束; (b) 应用层检查返回值 `success` 状态; (c) 使用数据库事务将检查+删除原子化
|
||||
4. **有子节点的父节点可能被删除(树结构完整性保护缺陷)** — ❌ 未修复
|
||||
- **数据库层缺失外键约束**: Notes 表 `pid` 列未定义 `FOREIGN KEY ... ON DELETE RESTRICT`([db/create_sqlite3.sql](../db/create_sqlite3.sql))。
|
||||
- **应用层 TOCTOU 竞态**: 子节点查询(SELECT)与删除(DELETE)是两个独立 RPC,无事务保护。
|
||||
- **错误处理缺陷**: 多处直接访问 `notes['data']` 而未先确认 `success`(如 [dnfs.py:387](dnfs.py#L387)、[L519](dnfs.py#L519)),`success=False` 时可能抛 KeyError。
|
||||
|
||||
### 🟠 P1 — 高危问题
|
||||
|
||||
5. **Token 无过期校验** — payload 中包含 `iat`/`created_at`/`keep_time` 字段,但 `decode_token()` 完全不检查,泄露的 Token 可永久使用
|
||||
6. **请求体大小无限制** — Waitress 未配置 `max_request_body_size`,可能被大请求体 DoS;建议设置 10MB 上限与 WAF 对齐
|
||||
7. **`log_client.py` 配置与主模块不一致** — [log_client.py:32-33](log_client.py#L32-L33) 直接读 `DB_HOST`/`DB_PORT` 环境变量,不从 confs 拉取配置,可能连错地址
|
||||
8. **`update_note` 修改 pid 时未验证目标父节点存在** — `add_note` 会检查 pid≠0 时父节点是否存在,但 `update_note` 直接更新不检查,可将节点移动到不存在的父节点下
|
||||
9. **db RPC 客户端未启用启动等待** — `_rpc = RpcClient(...)` 未设置 `retry_on_refused=True`,db 未就绪时首次请求失败(虽然 confs 连接启用了重试)
|
||||
10. **`delete_note`/`update_note` 中 `decode_token` 无 try-except** — `get_notes` 中包裹了异常处理,但这两个函数没有,base64 解码/JSON 解析错误直接抛 500
|
||||
11. **`note_id` 缺少强制类型转换** — `note_id: int = data.get('id')` 只是类型提示,不会实际转换;传入字符串 `"5"` 依赖 SQLite 类型亲和性隐式匹配
|
||||
5. **Token 无过期校验** — ✅ 已修复(gateway `/whoami` 基于 `created_at` + `keep_time` 统一校验,dnfs 不再自行处理)
|
||||
6. **请求体大小无限制** — ❌ 未修复([dnfs.py:723](dnfs.py#L723) `serve(app, ...)` 未配置 `max_request_body_size`,建议设 10MB 与 WAF 对齐)
|
||||
7. **`log_client.py` 配置与主模块不一致** — ❌ 未修复([log_client.py:32-33](log_client.py#L32-L33) 直接读 `DB_HOST`/`DB_PORT` 环境变量,不拉取 confs)
|
||||
8. **`update_note` 修改 pid 时未验证目标父节点存在** — ❌ 未修复([update_note](dnfs.py#L573) 直接更新,不检查 pid≠0 时父节点是否存在)
|
||||
9. **db RPC 客户端未启用启动等待** — ❌ 未修复([dnfs.py:133](dnfs.py#L133) `_rpc = RpcClient(...)` 无 `retry_on_refused=True`)
|
||||
10. **`delete_note`/`update_note` 中 `decode_token` 无 try-except** — ✅ 基本解决(两函数均由 `@_handle_exp` 统一捕获异常,不再裸抛 500)
|
||||
11. **`note_id` 缺少强制类型转换** — ❌ 未修复([dnfs.py:474](dnfs.py#L474)、[L577](dnfs.py#L577) 仅类型提示,依赖 SQLite 类型亲和性隐式匹配)
|
||||
|
||||
### 🟡 P2 — 中危问题
|
||||
|
||||
12. **错误响应泄露内部信息** — 多处错误消息拼接 `request.remote_addr` 和用户名返回给客户端(如"未授权访问,从xxx"),应仅记录到服务端日志
|
||||
12. **错误响应泄露内部信息** — 多处错误消息拼接 `request.remote_addr` 和用户名返回给客户端,应仅记录到服务端日志
|
||||
13. **CORS 配置过于宽松** — `CORS(app)` 默认允许所有来源(`*`),建议限制为实际前端域名
|
||||
14. **`_call_dbv2` 返回值访问前未统一检查 success** — 多处直接访问 `notes['data']` 而不先确认 `success==True`
|
||||
15. **`pid` 参数判断逻辑不严谨** — `if not pid: pid = 0` 会将空字符串/None 等非法输入默默转为 0,掩盖参数错误
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ UniServe Framework 的应用层网关,C++20 实现。
|
||||
| 密码哈希 | PBKDF2-HMAC-SHA256,100000 轮,16 字节随机盐,32 字节哈希 |
|
||||
| 密码强度校验 | 至少 8 字符;大写/小写/数字/特殊字符中至少 3 类 |
|
||||
| 邮箱格式校验 | 正则匹配 `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$` |
|
||||
| 旧哈希自动迁移 | 登录时若发现 SHA256 旧哈希,验证通过后自动迁移为 PBKDF2 |
|
||||
| 旧哈希自动迁移 | 登录时若发现 SHA256 旧哈希,验证通过后自动迁移为 PBKDF2(⚠️ 当前源码中该迁移逻辑已被注释禁用,见 `main.cpp` L1102-1109,未生效) |
|
||||
| Token 机制 | `Base64(payload) + "_" + RSA-Sign(payload)`,验签 + 过期校验(默认 1 小时) |
|
||||
| 常数时间比较 | PBKDF2 验证用 `diff |= hash[i] ^ expected[i]` 防 timing attack |
|
||||
| CAPTCHA | 一次性、5 分钟过期,登录与注册均可选启用 |
|
||||
|
||||
+6
-10
@@ -49,18 +49,12 @@ static :9200 (Flask + CORS)
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `POST /api/hash` | SHA256 密码哈希 |
|
||||
| `POST /api/check_user` | 检查用户是否已注册(查询 gateway 数据库) |
|
||||
| `POST /api/reset_password` | 重置用户密码(直接操作 gateway 数据库) |
|
||||
| `POST /api/check_user` | 检查用户是否已注册(经 db:8005 服务 pymsg 调用查询) |
|
||||
| `GET /api/favicon?url=` | favicon 服务端代理(dnfs 链接卡片用;SSRF 防护:仅 http/https + 内网地址拦截 + 重定向校验,512KB 上限,缓存 1 天) |
|
||||
|
||||
### yhdet OAuth 路由
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `GET/POST /yhdet_callback` | yhdet.top OAuth 回调,自动注册/登录并跳转 dnfs 页 |
|
||||
| `POST /api/yhdet_login` | yhdet OAuth 用户名密码登录 |
|
||||
| `POST /api/yhdet_verify` | yhdet Token 验证 |
|
||||
| `POST /api/proxy_send_code` | yhdet 验证码代理转发 |
|
||||
> 以下路由已在安全审计修复中**禁用**(源码以 `#` 注释,不再对外暴露):
|
||||
> - `POST /api/reset_password` — 无认证可接管任意账户(原 P0-1)
|
||||
> - `GET/POST /yhdet_callback`、`POST /api/yhdet_login`、`POST /api/yhdet_verify`、`POST /api/proxy_send_code` — yhdet OAuth 系列(原 P0-2/3/4、NEW-1/2)
|
||||
|
||||
### dnfs 代理路由
|
||||
|
||||
@@ -73,6 +67,8 @@ static :9200 (Flask + CORS)
|
||||
|
||||
以上 4 个 dnfs 代理路由均支持 HttpOnly Cookie 模式:当请求携带 `q4_token` Cookie 时,自动转为 `Authorization: Bearer` 头转发。
|
||||
|
||||
> **CORS 例外**:全局 CORS 使用 `[cors] allowed_origins` 白名单;但上述 4 个 dnfs 代理路由的 OPTIONS 预检由 `_cors()` 处理,返回 `Access-Control-Allow-Origin: *`(通配)。若需收紧,应按白名单改造 `_cors()`(安全审计 NEW-3 未修复)。
|
||||
|
||||
> `/whoami` 已上移至 gateway,由 gateway 直接验签 Token 返回用户身份,不再经 static 代理到 dnfs。
|
||||
|
||||
## 功能
|
||||
|
||||
+3
-1
@@ -342,7 +342,7 @@ WAF 支持透明代理 WebSocket 连接(如 aibox 的 WebSocket 接口)。
|
||||
- CSP 仍含 `'unsafe-inline'`(为兼容 forgot.html 与 games/*.html 的内联脚本),待前端清理后可收紧。
|
||||
- HSTS 已启用(半年期,不含子域),站点全量 HTTPS 覆盖后可按需收紧。
|
||||
- logclient 日志推送每次新建 TCP 短连接,高并发拦截场景可能产生较多 TIME_WAIT;db 地址硬编码 `127.0.0.1:8005`,不支持环境变量配置。
|
||||
- **默认规则全部注释**:`rules/local/custom.conf` 中自定义规则默认注释,需手动取消注释或拉取 OWASP CRS 后才有实际拦截效果。启动时无 warning 提示。
|
||||
- **默认规则全部注释**:`rules/local/custom.conf` 中自定义规则默认注释,需手动取消注释或拉取 OWASP CRS 后才有实际拦截效果。启动时无 warning 提示。**注意:CRS 规则目录不入库,需先在部署机执行 `_fetch_crs.sh`;未拉取 CRS 时 WAF 开箱无拦截能力。**
|
||||
- 缺少 SIGINT/SIGTERM 优雅关闭,进程终止时进行中的请求被暴力中断,logclient 队列中未发送的日志可能丢失。
|
||||
- 标准日志(`log.SetOutput`)被静默到 `io.Discard`,net/http 的 panic 堆栈和 TLS 错误不可见。
|
||||
|
||||
@@ -355,6 +355,8 @@ WAF 支持透明代理 WebSocket 连接(如 aibox 的 WebSocket 接口)。
|
||||
- **feat**: `tuning.conf` 独立于自动生成的 `crs/coraza.conf`,重跑 `_fetch_crs.sh` 不会覆盖调优配置
|
||||
- 校验:SQLi / XSS / 路径遍历 / 命令注入均命中 949110 拦截,正常请求放行
|
||||
|
||||
> **⚠️ 生效前提**:CRS 规则目录 `rules/crs/` 被 `.gitignore` 排除,不入库。**必须先在某台机器上执行 `bash _fetch_crs.sh` 拉取 CRS 规则**,`crs/coraza.conf` 存在后 engine 才会自动加载并启用自动拦截。若未拉取 CRS,实际生效的仅有 `custom.conf`(默认全注释)+ `tuning.conf`(仅 setvar),**WAF 开箱无拦截能力**。部署时请务必先拉取 CRS 或启用 `custom.conf` 中的兜底规则。
|
||||
|
||||
### v1.2(2026-08-01)— WebSocket 支持 + 修复
|
||||
|
||||
- **feat**: WebSocket 透明代理支持,`responseRecorder` 实现 `http.Hijacker` 接口
|
||||
|
||||
Reference in new issue
Block a user