From 0348b959bb9b8084e6dffea238ccb5bd1a1a0aee Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Wed, 5 Aug 2026 16:26:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=20static=20?= =?UTF-8?q?=E4=B8=8E=20waf=20README=20=E8=87=B3=E5=AE=A1=E8=AE=A1=E5=90=8E?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - static: 移除/标注已禁用的 reset_password 与 yhdet 系列路由,更新 check_user 为经 db 服务查询,补充代理路由 CORS 通配例外说明 - waf: 明确 CRS 需先 _fetch_crs.sh 才生效,未拉取时 WAF 开箱无拦截能力 --- static/README.md | 16 ++++++---------- waf/README.md | 4 +++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/static/README.md b/static/README.md index 9a46100..f33f0fd 100644 --- a/static/README.md +++ b/static/README.md @@ -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。 ## 功能 diff --git a/waf/README.md b/waf/README.md index e0ac488..ee5dbed 100644 --- a/waf/README.md +++ b/waf/README.md @@ -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` 接口 -- 2.54.0 From 9c73d7058256f0a242d8d6ef3c2a250d38791ad5 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Wed, 5 Aug 2026 16:29:29 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(gateway):=20=E6=A0=87=E6=B3=A8?= =?UTF-8?q?=E6=97=A7=E5=93=88=E5=B8=8C=E8=87=AA=E5=8A=A8=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=B7=B2=E8=A2=AB=E6=B3=A8=E9=87=8A=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README 安全特性中"旧哈希自动迁移"实际在 main.cpp L1102-1109 被注释,标注其当前未生效。 --- gateway/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gateway/README.md b/gateway/README.md index 347a3af..16acd4a 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -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 分钟过期,登录与注册均可选启用 | -- 2.54.0 From 6483719551da701791e9a7542124e2fec17112b7 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Wed, 5 Aug 2026 16:34:34 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs(dnfs):=20=E5=90=8C=E6=AD=A5=20README?= =?UTF-8?q?=20=E8=87=B3=E5=AE=A1=E8=AE=A1=2008-03=20=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E7=9C=9F=E5=AE=9E=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主干: Token 改为委托 gateway /whoami 验签描述 - 已知问题: 标注 P0 #1/#3、P1 #5/#10 已修复,P0 #2 基本修复,其余未修复项保留 --- dnfs/README.md | 52 ++++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/dnfs/README.md b/dnfs/README.md index 0a7b4df..54202dd 100644 --- a/dnfs/README.md +++ b/dnfs/README.md @@ -45,7 +45,7 @@ Dendrachy Network File-System,基于父子关系(pid)组织的树状节点 Authorization: Bearer _ ``` -- 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,掩盖参数错误 -- 2.54.0