Compare commits

...
3 Commits
Author SHA1 Message Date
qing c07ad7ac39 docs: 新增架构/模块文档与项目评估资料
- docs/ARCHITECTURE.md: 系统架构说明
- docs/MODULES.md: 模块清单与职责
- docs/blackbird_project_profile.md: 项目画像
- docs/blackbird_project_evaluation.md: 项目评估
- docs/reports/fsys_public_frontend_research_20260810.md: fsys 公开前端调研
2026-08-10 01:27:48 +08:00
qing 685c5c1754 refactor(libmsg): 将审计结论合入代码注释并精简审计报告
- 在 libmsg 各头文件修复位置补充 [libmsg-audit <编号>] 标签(27 处),
  覆盖 16 条安全审计发现(P0-1/P0-2/P1-1~3/P2-1~8/P3-1~3),
  将已完成修复锚定到源码
- 从审计文档删除 335 行逐条详细发现(问题描述/修复方案/触发场景/代码块),
  替换为 16 行「已解决发现」紧凑台账 + 源码定位指针
- 文档保留:严重等级表、修复进度汇总、已有安全机制、已知限制 L-1~L-12、
  变更记录、审计结论
2026-08-10 01:26:10 +08:00
qing 373045f0a3 pymsg: 将已修复安全审计项合入源码注释, 精简审计报告
- 将 P0-1/P0-2/P1-1/P1-2/P1-3/P1-5 共 6 条已修复发现作为
  [SECURITY AUDIT ... ✅ 已修复] 注释合入 pymsg 源码对应位置
- 重写 pymsg 审计报告, 删除已合并条目, 仅保留 10 条未修复项待跟踪
2026-08-10 01:08:55 +08:00
14 changed files with 1146 additions and 530 deletions

No files matched your search

+252
View File
@@ -0,0 +1,252 @@
# Blackbird 架构文档(UniServe Framework)
> 权威架构参考。本文以 `core/init.txt`(实际运行清单)与 `git HEAD` 为事实来源。
> 与根目录 `README.md` 的偏差见文末「§10 与 README 的偏差」。
> 最后核对于:2026-08-10。
---
## 1. 文档目的与范围
Blackbird 是一套 **进程托管式轻量微服务** 系统(自研 UniServe Framework),用于支撑一个带内容审核的社区/笔记类 Web 应用及其周边 AI/嵌入式子系统。本文描述:
- 系统上下文与边界
- 部署拓扑(容器级)
- 关键请求链路
- 服务清单(含语言、端口、职责、当前状态)
- 共享基础库与横切关注点
- 关键架构决策(ADR 摘要)
- 演进策略与已知漂移
**不在本文范围**:单模块内部算法、前端页面细节、硬件机器人/帧缓冲子系统的实现。
---
## 2. 系统上下文(System Context)
```
浏览器 / 客户端
│ HTTPS :443
▼
┌─────────────────────────────────────────────────────────────┐
│ Blackbird 系统边界 │
│ waf → ca → gateway → {fsys, dnfs2, core} │
│ 内部:db2 / ai_review / smtp / captcha2 / core(confs) │
│ 独立:aibox(+BotOps) / bot4cj / lci │
└─────────────────────────────────────────────────────────────┘
│ pymsg/TCP (内部 RPC) │ SMTP (外部邮件)
▼ ▼
db2 / ai_review / smtp 邮件服务商 (注册验证码)
```
**参与者**:
- 终端用户:经 `:443` 访问社区首页、登录/注册、树状笔记。
- 运维/开发者:经 `core:5200` 管理接口启停子进程。
- 外部依赖:SMTP 邮件服务商(注册验证码)、DeepSeek API(ai_review)。
---
## 3. 部署拓扑(Container Diagram)
```
┌──────────────────────────────────────────────────┐
│ core (PMC / Nidus) │
│ 进程托管 :5200 (HTTP 管理) + 内嵌 confs :8010 │
│ 读 init.txt 拉起并守护下列子进程 │
└───────────────────────┬──────────────────────────┘
│ 启动 / 守护
┌────────────────┬───────────────────────┼──────────────────┬─────────────────┐
▼ ▼ ▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ redirect80 │ │ waf │ │ ca │ │ gateway │ │ fsys │
│ :80 │ │ :443 │ │ :8443 │ │ :8445 │ │ :9200 │
│ Python │ │ Go │ │ C++20 │ │ C++20 │ │ C++20 │
└─────┬──────┘ └─────┬──────┘ └─────▲─────┘ └─────▲─────┘ └─────▲─────┘
│ 301/308 │ TLS+coraza │ auth+forward │ loopback │ proxy
└───────────────►│ │ │ pure-fwd │ notes
└────────────────────►│ │ └────┬────┘
│ │ │ pymsg
┌────────┴────────┐ ┌─────┴────────┐ ┌────────▼─────┐
│ dnfs2 :8006 │ │ core :5200 │ │ dnfs2 :8006 │
│ C++20 │ │ (管理/守护) │ │ (经 fsys 代理)│
└───────▲────────┘ └──────────────┘ └──────────────┘
│ pymsg
┌────────────┼─────────────┐
▼ ▼ ▼
┌────────────┐ ┌───────────┐ ┌───────────┐
│ db2 :8005 │ │ai_review │ │ smtp │
│ C++20 pymsg│ │ :8007 Py │ │ :8008 Py │
│ +Logs 表 │ │ RAG+LLM │ │ pymsg │
└─────▲──────┘ └─────▲─────┘ └─────▲─────┘
│ │ 审核 │ 发码
│ └─────────────┘
┌─────┴──────────────────────────────┐
│ captcha2 :8009 (C++20, pymsg) │
└─────────────────────────────────────┘
独立 / 外设(不在主链路):
aibox :8765 (Python WebSocket, 独立产品)
BotOps (aibox 的 TUI 前端)
bot4cj (Cangjie + 树莓派 I2C, 硬件)
lci (C++ /dev/fb0, 嵌入式 GUI)
actpred / memdb / libtry (实验 / 支撑库)
日志链路: 各服务 log_client ──pymsg──► db2:8005 Logs 表 (fail-safe, db 不可用时静默丢弃)
配置链路: core 内嵌 confs :8010 ──pymsg get──► 各服务 (confs/configs/*.ini, mtime 自动重载)
```
> 注:`core` 启动参数为 `./nidus.out -i init.txt --serve 5200 --confs 8010`,`--confs` 启用内嵌配置服务(由 confs2 合并而来,与 Python confs 协议兼容)。
---
## 4. 关键请求链路
### 4.1 正常访问(已登录)
1. 客户端若以 HTTP 访问 `:80`,`redirect80` 重定向到 HTTPS `:443`(GET/HEAD→301,其余→308 保留 body)。
2. 请求打到 `waf:443`(Go/coraza):终止 TLS,运行 SecLang 规则;命中→403。
3. 未命中规则 → 明文 HTTP 转发到 `ca:8443`,注入 `X-Forwarded-For` / `X-Forwarded-Proto`。
4. `ca` 验签 Token,注入 `X-Auth-User` / `X-Auth-Admin`,转发到 `gateway:8445`(仅 loopback)。
5. `gateway` 检查:显式 `Target-Url` 必须带 `X-Auth-User`(否则 401),且必须在 `Proxy.opened` 白名单(否则 403);转发到 `fsys:9200` / `dnfs2:8006` / `core:5200`。
6. `fsys` 处理前端资源,笔记类请求代理到 `dnfs2:8006`。
### 4.2 注册 / 登录(ca 负责)
- `/login`、`/register`、`/reset_*`、`/captcha/image`、`/whoami` 由 `ca:8443` 直接处理(不再经 gateway 鉴权层)。
- 密码:PBKDF2-HMAC-SHA256(10 万轮 + 16B 随机盐),常数时间比较。
- Token:`Base64(payload) + "_" + RSA-Sign(payload)`,默认 1h 过期;RSA 密钥内存生成,重启即全失效;jti 黑名单 + 用户版本号双轨吊销。
- 注册邮件验证码:ca 经 pymsg/TCP 调 `smtp:8008` 发送与校验;登录强制 CAPTCHA(`captcha2:8009`)。
- 用户数据:ca 经 pymsg/TCP 调 `db2:8005` 存取(`Users` 表,首位注册者即管理员)。
- 速率限制:登录 5/5min、注册 3/10min、发码 IP+邮箱双窗口,按 `X-Forwarded-For` 计数。
### 4.3 笔记写入(含内容审核)
`fsys → dnfs2:8006` → 新增节点前 `dnfs2` 调 `ai_review:8007 /review`(fail-closed,异常一律拦截)→ 审核通过后经 pymsg 调 `db2:8005 execute` 持久化。
### 4.4 日志 / 配置(横切)
- 各服务内置 `log_client`(异步队列 + 后台批量 flush)→ pymsg 写 `db2:8005` 的 `Logs` 表;`db2` 自带 30 天/100 万条清理线程。
- 各服务启动时经 pymsg `get` 从 `core:8010` 拉取自身 `.ini`;`confs/configs/` 下 mtime 变化自动重载;环境变量覆盖;命令行参数最高优先级。
---
## 5. 服务清单(当前事实状态)
| 服务 | 语言 | 端口 | 绑定 | 职责 | 状态 |
|------|------|------|------|------|------|
| **core** | C++20 | 5200 / 8010 | 5200 管理 / 8010 loopback | 进程托管(PMC) + 内嵌 confs 配置服务 | 生产 |
| **redirect80** | Python | 80 | 0.0.0.0 | HTTP→HTTPS 重定向 | 生产 |
| **waf** | Go | 443 | 0.0.0.0 | 边缘网关:TLS 终止 + coraza WAF | 生产 |
| **ca** | C++20 | 8443 | 127.0.0.1 | 中央认证 + 身份头注入 + 前置转发 | 生产 (v1.0.0) |
| **gateway** | C++20 | 8445 | 127.0.0.1 | 纯转发反向代理(loopback 强制自检) | 生产 (v0.9.0) |
| **fsys** | C++20 | 9200 | 127.0.0.1 | 前端托管 + dnfs 代理(static 的 C++ 重写) | 生产 |
| **dnfs2** | C++20 | 8006 | 127.0.0.1 | 树状笔记 CRUD | 生产(dnfs 的 C++ 重写) |
| **db2** | C++20 | 8005 | 127.0.0.1 | 通用 DB(pymsg execute) + 日志汇聚 | 生产(db 的 C++ 重写) |
| **ai_review** | Python | 8007 | 127.0.0.1 | AI 内容审核(DeepSeek + ChromaDB RAG) | 生产 |
| **smtp** | Python | 8008 | 127.0.0.1 | 邮件验证码后端(pymsg) | 生产(smtp3 C++ 重写待上线) |
| **captcha2** | C++20 | 8009 | 127.0.0.1 | 图形验证码(pymsg) | 生产(captcha 的 C++ 重写) |
| **aibox** | Python | 8765 | 127.0.0.1 | 独立 AI 智能体(WebSocket) | 独立产品 |
| **BotOps** | Python | — | — | aibox 的 TUI 前端 | 独立 |
| **bot4cj** | Cangjie | — | 树莓派 I2C | 机器人脚本解析器 | 独立硬件 |
| **lci** | C++ | — | /dev/fb0 | 嵌入式帧缓冲 GUI | 独立硬件 |
| **actpred** | Python | — | — | ML 预训练/推理 | 实验 |
| **memdb** | C++20 | — | — | 内存数据库 | 实验/支撑 |
| **libtry** | C | — | — | 嵌入式脚本解释器 | 支撑 |
### 已被替代、不应再部署的遗留模块(不在 init.txt)
| 遗留 | 替代 | 说明 |
|------|------|------|
| `static` (Python) | `fsys` | 前端托管已 C++ 重写 |
| `db` (Python) | `db2` | DB 服务已 C++ 重写 |
| `dnfs` (Python) | `dnfs2` | 笔记服务已 C++ 重写 |
| `captcha` (Python) | `captcha2` | 验证码已 C++ 重写 |
| `confs` (Python) | 并入 `core` | 配置服务内嵌进 core(ConfsTask) |
| `smtp3` (C++) | (预备) | 代码就绪,init.txt 仍用 Python `smtp`,尚未平替 |
---
## 6. 共享基础库
| 库 | 语言 | 说明 |
|----|------|------|
| libth | C++20 | 线程 / 条件变量 / FSM / 任务接口 |
| libhttp | C++20 | HTTP/WebSocket(Boost.Beast,服务端 + 客户端) |
| libcrypt | C++20 | AES_CBC / RSA / SHA256 / Base64 |
| libsqlite3 | C++20 | SQLite3 header-only 封装 |
| libmsg | C++20 | pymsg 协议 C++ 实现(4B 大端长度前缀 + UTF-8 JSON),与 Python pymsg 字节级兼容,提供 RpcClient/RpcServer/AsyncQueue |
| libtry | C | 嵌入式脚本解释器(递归下降,中文标识符) |
| pymsg | Python | 自研 RPC 基础库(`[4B 大端长度][UTF-8 JSON]`),Python 侧 db/log_client 依赖 |
**协议约定**:内部 RPC 统一用 pymsg(TCP 长度前缀帧 + JSON);对外 HTTP;ca↔gateway 之间为明文 HTTP(TLS 已由 waf 终止)。
---
## 7. 横切关注点
### 7.1 安全分层(纵深防御)
- **边缘**:`waf` 终止 TLS + coraza 规则(本地自定义 100000–199999 / OWASP CRS 900000–999999),命中即 403。
- **应用层**:`ca` 负责鉴权(PBKDF2 + RSA Token + CAPTCHA + 速率限制);`gateway` 仅 loopback,身份完全信任 ca 注入头。
- **网络隔离**:`db2/ai_review/smtp/captcha2/confs` 仅监听 loopback,不经 gateway 直访(`Proxy.closed`)。
- **fail-closed 文化**:`ai_review` 异常即拦截;`dnfs2` Token 验签失败返 503;`gateway` 错误响应脱敏;SMTP 内部错误对外统一通用码(如 3103)。
### 7.2 可观测性
- 集中式日志:所有服务经 `log_client` 异步写 `db2` 的 `Logs` 表(service/level/message/trace_id/extra)。
- **缺口**:无分布式追踪(trace_id 仅记录未串联)、无指标/监控中间件、无告警。
### 7.3 配置管理
- `core` 内嵌 confs(`:8010`)统一分发 `confs/configs/*.ini`;内存缓存 + mtime 重载;路径遍历防护。
- 三层覆盖:confs 默认值 → 环境变量 → 命令行参数。
### 7.4 弹性 / 容错
- 启动顺序敏感:`core` 拉起顺序为 db2→ai_review→dnfs2→smtp→fsys→waf→ca→gateway→captcha2→aibox→redirect80;pymsg 客户端带 `retry_on_refused` 等依赖就绪。
- 日志 fail-safe:db2 不可用时静默丢弃,不影响业务。
- 有状态单点:`smtp` / `captcha2` 验证码存进程内存,**不支持多实例水平扩展**(需换 Redis 才能扩)。
---
## 8. 关键架构决策(ADR 摘要)
**ADR-1|认证从 gateway 剥离为独立 ca 模块(v0.9.0 / ca v1.0.0)**
- 背景:gateway 同时承担鉴权与转发,职责过重、RSA 密钥与业务逻辑耦合。
- 决策:ca 独立负责 Token 签发/验签、登录/注册/限流/CAPTCHA,gateway 退居 `127.0.0.1:8445` 纯转发。
- 后果:+1 网络跳数;ca 成为安全边界(loopback + strip-then-set);gateway 不再依赖 libcrypt/OpenSSL。
**ADR-2|核心服务 C++20 重写(db→db2, dnfs→dnfs2, static→fsys, captcha→captcha2, confs→core;smtp3 就绪)**
- 背景:Python 服务依赖 venv/pip/Flask,且部分存在 P0/P1 安全问题。
- 决策:用 libhttp+libmsg+boost 重写,接口字节级兼容(pymsg/HTTP 路由不变),可平替。
- 后果:去掉 Python 运行时依赖、修复安全缺陷、提升吞吐;过渡期需双版本并存维护;ai_review/aibox/smtp/redirect80 仍保留 Python。
**ADR-3|confs 配置服务内嵌进 core**
- 背景:独立 confs 进程增加编排复杂度。
- 决策:confs2 合并为 `core` 的 `ConfsTask`,由 `--confs <port>` 启用。
- 后果:少一个守护进程;core 略微变重;配置协议与目录结构不变。
**ADR-4|pymsg 作为内部 RPC 标准**
- 决策:内部数据访问统一 `[4B 大端长度][UTF-8 JSON]` over TCP,C++/Python 双实现字节级兼容。
- 后果:业务服务不直接连数据库,数据访问层解耦;引入轻量自研协议(无现成生态)。
---
## 9. 演进策略与已知缺口
- **无服务注册发现 / 容器编排**:服务地址在 `waf/config.yaml` 与 `gateway` 的 `Proxy.*` 中静态配置,由 `core` 进程托管。适合个人/中小规模 DIY,但扩展需手工。
- **smtp3 待上线**:C++ 重写已完成并通过安全审计,但 init.txt 仍指向 Python `smtp`,建议择机平替以消除最后一块 Python 有状态服务。
- **可观测性缺口**:缺 tracing/metrics/告警,建议后续引入轻量方案(或复用 db2 Logs + 定时聚合)。
- **水平扩展限制**:smtp/captcha 内存态为单点,若需多实例需引入 Redis 等共享存储。
---
## 10. 与 README.md 的偏差(待同步)
以下为本文相对于根 `README.md` 的纠正点,建议择机回填 README:
1. **端口**:README 端口表写 `gateway 8443`,实际 `ca:8443`、`gateway:8445`(v0.9.0 起)。README 正文已正确,仅端口表需改。
2. **前端模块**:README 仍列 `static`(Python)为主,实际生产为 `fsys`(C++ 重写),`static` 已废弃。
3. **C++ 重写版未进主表**:README 模块表仍以 `db`/`dnfs`/`captcha`/`confs` 为主,实际生产跑 `db2`/`dnfs2`/`captcha2`,`confs` 已并入 `core`。
4. **init.txt 不含 confs**:独立 confs 服务已不用,配置由 `core --confs 8010` 内嵌提供。
5. **fsys README 图示滞后**:`fsys/README.md` 架构图仍写 `gateway :8443`,应为 `ca:8443 → gateway:8445`。
6. **工作记忆偏差**:项目记忆中的 `gateway:8443 → fsys / dnfs` 与 `confs:8010` 需更新为 `ca:8443 → gateway:8445`、`dnfs2`、confs 内嵌 core。
---
## 11. 文档管理约定(遵循既有规则)
- 全局架构文档置于 `docs/`(本文件)。
- 模块细节写在各模块目录 `README.md`。
- 审计/审查/报告类文档分别入 `docs/security`、`docs/reviews`、`docs/reports`。
+89
View File
@@ -0,0 +1,89 @@
# 模块清单(blackbird / micro_servers)
> 一套 DIY 的轻量级「进程托管式微服务」(UniServe Framework)。
> 混合语言:C++20(core/gateway/dnfs2/db2/lib*)、Python(fsys/dnfs/db/ai_review/smtp/captcha/confs/redirect80/BotOps/aibox/actpred/pymsg)、Go(waf)、仓颉/Cangjie(bot4cj)。
> 由 `core` 读 `init.txt` 拉起并守护各子进程;`waf:443 → gateway:8443 → 业务服务` 为对外主链路。
## 一、核心与编排
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| core | C++20 | 5200 | 进程托管服务 PMC / Nidus,读 `init.txt` 拉起守护子进程,提供 HTTP 管理接口(`--send/--exec/--list/--kill/--serve`) |
## 二、入口与网关层
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| redirect80 | Python | 80 | HTTP→HTTPS 重定向(GET/HEAD 301,其余 308 保留 body);纯标准库 `http.server` |
| waf | Go | 443 | 唯一对外入口,终止 TLS + coraza 规则匹配(SecLang)+ 反代到 gateway;`kill -HUP` 热加载;规则库 `waf/rules/` |
| gateway | C++20 | 8443 | 应用层网关(内部 HTTP),注册/登录鉴权、PBKDF2 密码哈希、RSA Token 签发校验、Cookie 认证、速率限制、反向代理(`Proxy.opened` 白名单) |
## 三、业务服务(对外,经 gateway 代理)
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| fsys | Python/Flask | 9200 | 前端托管(社区首页/静态页面/媒体流/文件浏览);**原为 static,2026-08 重命名为 fsys**(core/init.txt 已同步),过渡期仍保留 legacy `static/` 目录存放 `fsys.py` |
| dnfs | Python/Flask | 8006 | 树状菜单(Dendrachy Network File-System),基于 pid 的节点树 CRUD,新增节点前调 ai_review 审核 |
| dnfs2 | C++20 | 8006 | dnfs 的 C++20 重实现(libhttp + libmsg),接口兼容可平替,修复 P0/P1(fail-closed 验签/所有权校验/脱敏) |
| ai_review | Python/Flask | 8007 | AI 内容审核,DeepSeek + ChromaDB RAG(色情/违禁药/赌博/诈骗/仇恨暴力),fail-closed 宁可误拦 |
## 四、基础设施 / 支撑服务(仅内部调用)
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| db | Python/pymsg | 8005 | 通用数据库服务,SQL-over-TCP(SQLAlchemy Core 参数化防注入),内置集中式日志(Logs 表 + 自动清理) |
| db2 | C++20 | 8005 | db 的 C++20 重实现(libmsg + boost::json + libsqlite3),`execute` 路由/where 语法/响应格式对齐,可平替 |
| confs | Python/pymsg | 8010 | 统一配置分发,集中存放各服务配置(`confs/configs/`),内存缓存 + mtime 自动重载,路径遍历防护 |
| smtp | Python/pymsg | 8008 | 邮件服务,注册邮箱验证码后端(路由 send_email/verify_email/health),6 位码内存存储、冷却/限流/熔断 |
| captcha | Python/pymsg | 8009 | 验证码服务,图形验证码(一次性、5 分钟过期),经 gateway `GET /captcha/image` 代理给浏览器 |
## 五、AI 智能体与前端
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| aibox | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度/前端与语音客户端 |
| BotOps | Python/Textual | — | aibox 的终端 TUI 前端,WebSocket 连接 aibox,Markdown 增量流式渲染、思考模式、任务侧栏、自动重连 |
| actpred | Python/PyTorch | — | 动作预测(图像 + 动作历史 → 预测下一个动作),bot4cj 的「大脑」配套;支持从 `.mt` 脚本预训练 |
## 六、机器人硬件与嵌入式
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| bot4cj | 仓颉/Cangjie | — (树莓派 I2C) | 机器人硬件脚本解析器,解析 `.mt` 驱动 PCA9685 舵机 / MPU6050 陀螺仪 / 扬声器,面向 Dashun 蜘蛛机器人 |
| lci | C++ | — (/dev/fb0) | 嵌入式帧缓冲图形界面(无需 X11/Wayland),矢量 GB2312 中文字库、图片渲染、实时数据可视化 |
| 3d | — (资产) | — | 机器人 / 机械臂 CAD 模型(STEP/STL/SLDPRT),bot4cj 配套硬件资产 |
## 七、共享基础库
| 模块 | 语言 | 说明 |
|------|------|------|
| libth | C++ | 线程库:`Thread`、`Cv_wait`、`Fsm`、`ITask` |
| libhttp | C++ | HTTP/WebSocket 库(Boost.Beast):`HttpServer`(含 WS 服务/代理)、`HttpClient`(含 WS 客户端) |
| libcrypt | C++ | 加密库:`AES_CBC`、`MyRSA`、`SHA256`、`Base64`、`Crypto_Basic` |
| libsqlite3 | C++ | SQLite3 header-only 封装 |
| libmsg | C++ | pymsg 协议 C++ 实现(4B 大端长度前缀 + UTF-8 载荷),`MsgClient/Server`、`RpcClient/Server`、`AsyncQueue`,与 Python pymsg 字节级兼容 |
| libtry | C | 嵌入式脚本解释器(~741 行),手写递归下降,支持中文标识符,用于嵌入式操作界面 shell |
| pymsg | Python | 自研 RPC 基础库(`[4B 长度][UTF-8 JSON]`),db 服务端与各 `log_client.py` 的基础依赖(setuptools 包) |
| memdb | C++ | 内存数据库(include/src),内部组件,未在主 README 模块表列出 |
## 八、测试与文档
| 模块 | 说明 |
|------|------|
| tests | 黑盒测试套件(`test_db_*`/`test_gateway_*`/`test_waf_*`/`test_dnfs_*`),子进程 + HTTP 验证,前后自动备份恢复 db |
| docs | 文档聚合(security/ 审计、reviews/ 审查、reports/ 分析、INDEX.md),41+ 篇 md |
## 九、根级构建与部署脚本
- `_inst.sh`:一键按依赖顺序构建安装(库先行,core 最后)
- `_setup.deb.sh` / `_setup.rhel.sh`:系统依赖安装(Debian/Ubuntu、RHEL 系)
- `docker.sh` / `docker-compose.yml`:容器化部署
- 部署路径约定 `/opt/micro_servers/`,`core` 的 `init.txt` 即从该路径拉起其余服务
## 关键链路速记
- **请求主链路**:client → `redirect80`(:80→443) → `waf`(:443 TLS + WAF) → `gateway`(:8443 鉴权) → `fsys`(:9200) / `dnfs`(:8006)
- **日志链路**:各服务 `log_client.py`(异步批量)→ `db:8005` Logs 表(fail-safe,db 不可用时静默丢弃)
- **配置链路**:业务服务 → `confs:8010`(内存缓存 + mtime 重载,confs 不可用时回退本地配置)
- **数据/审核/邮件**:`dnfs` → `db:8005`(pymsg 持久化);`dnfs` → `ai_review:8007`(审核);注册邮件 `gateway` → `smtp:8008`
- **端口一览**:80 redirect80 · 443 waf · 8443 gateway · 5200 core · 9200 fsys · 8006 dnfs/dnfs2 · 8005 db/db2 · 8007 ai_review · 8009 captcha · 8008 smtp · 8010 confs · 8765 aibox
+37
View File
@@ -0,0 +1,37 @@
# Blackbird / micro_servers 项目红队评估
> 评估对象:`docs/blackbird_project_profile.md`(2026-08-10 advisory 整合稿)
> 评估视角:材料红队(事实 / 推断 / 未知 + 矛盾与缺证)
> 评估时间:2026-08-10
## 一句话结论
架构主张清晰、文档可审计性极高的自托管轻量微服务框架;但"安全评级 A-"缺乏代码级实证,3 项高危整改全部处于 `human_review_pending`,当前不适合暴露于多租户或不可信内网。
## 优势(已确认事实)
- **定位成立**:砍掉 K8s 的服务注册/编排/消息总线,保留微服务拆分与统一入口收益,自托管 + MIT,价值主张自洽。
- **可审计性远超同类个人项目**:41+ 篇安全/分析文档 + 24 篇逐模块审计报告,风险闭环有基底。
- **协议基石扎实**:pymsg/libmsg 字节级兼容的 `[4B 长度][UTF-8 JSON]` 帧,支撑 v1/v2 双栈平滑迁移。
- **认证设计不弱**:PBKDF2 10 万轮、常数时间比较、RSA 签名 Token、Cookie 安全属性、fail-closed 边界。
## 关键风险(矛盾与缺证)
1. **评级名实不符**:称"B+→A-",但 3 高危(WAF 失效 / core root RCE / 内部零信任缺失)均未核实修复。A- 更像目标而非现状。
2. **WAF 实际不拦截**:定位"唯一对外入口 + coraza",但 `waf/rules/local/custom.conf` 的 6 条 SecRule 全被注释、无 `crs/` 目录 → 实际仅 TLS 终止 + 响应头,SQLi/XSS/命令注入零防护。
3. **core 本地 root RCE**:`parse_data_from_mq` 对 list/kill/exec 未鉴权即经 `/bin/sh -c` 执行,且 core 以 root 运行。这是致命项却被标 `human_review_pending`。
4. **内部零信任缺失**:pymsg 无握手/无鉴权/明文无 TLS,db `execute` 接受任意 action/table/where,完全依赖 127.0.0.1 隔离。
5. **端口口径漂移**:MODULES.md 记 gateway:8443,README 记 8445,未同步。
6. **密钥变量静默失效**:`.env.example` 写 `OPENAI_API_KEY`,代码读 `DEEPSEEK_API_KEY`。
7. **质量债**:v1/v2 双实现(db/db2、dnfs/dnfs2、pymsg/libmsg…)并存,无单元测试、无 CI(无 `.github/workflows`),回归与语义漂移风险高。
8. **部署半成品**:docker-compose 仅纳 3 服务,生产以 systemd 为准——容器化链路不完整。
## 缺证项(需人工复核)
- 3 高危是否在 2026-08-08 之后的提交中已修复(研究报告基于快照,文档未给代码证据)。
- docker-compose 补全状态、MODULES.md 端口同步状态。
## 优先级建议(收口路径)
- P0:启用 WAF 规则 + 补 `crs/`(直击高危 1)。
- P0:core MQ 落地 `verify_admin_token` 并降权运行(直击高危 2)。
- P1:内部服务加调用方鉴权 / pymsg 上 mTLS 或 Unix socket(直击高危 3)。
- P2:ai_review `/review` 加鉴权 + 结构化提示词;统一 `DEEPSEEK_API_KEY`;引入单元测试与 CI;双实现收口单轨。
## 成熟度判定
**个人/小团队可信内网自用 = 可用;暴露公网或多租户 = 不建议。** 文档完备度是亮点,代码整改完成度与测试覆盖是短板。
+372
View File
@@ -0,0 +1,372 @@
# Blackbird / micro_servers 项目资料长文档
> 文档类型:项目资料 / 综合参考手册(面向开发者、维护者、审计者与潜在贡献者)
> 资料来源:仓库主 `README.md`、各模块 `README.md`、`docs/INDEX.md`、`docs/MODULES.md`、`docs/security/*`、`docs/reviews/*`、`docs/reports/blackbird_research_report.md`、`docs/security/SECURITY_HARDENING_PLAN_20260803.md`、`docs/blackbird_promo_pack.md`
> 整理日期:2026-08-10
> 文档状态:`advisory`(基于仓库现有文档整合;涉及最新代码整改状态的部分标注为 `human_review_pending`,需结合代码人工复核)
---
## 0. 文档说明
本文档将 Blackbird(仓库名 `micro_servers`,框架名 **UniServe Framework**)过去分散在 40+ 篇说明、审计与报告中的信息,整合为一份可按章节查阅的项目资料。
- **事实边界**:所有模块职责、端口、协议描述以主 `README.md`(2026-08-10 口径,已含 `ca` 中央认证模块)为准;安全发现以 `blackbird_research_report.md`(2026-08-08 快照)与 `docs/security/` 逐模块审计为准。
- **口径差异提醒**:`docs/MODULES.md` 仍记 `gateway:8443`,属 `ca` 认证前置化之前的旧口径;当前拓扑为 `waf:443 → ca:8443 → gateway:8445`,以主 `README.md` 为准。
- **质量状态约定**:基于文档推断与整合的结论标记为 `advisory`;涉及"代码是否已修复某高危项"的判断标记为 `human_review_pending`,因其需对照最新提交逐文件核验。
---
## 1. 项目概述
### 1.1 一句话定位
Blackbird 是一套 **DIY 的轻量级"进程托管式微服务"框架**,用 C++20 / Python / Go / 仓颉(Cangjie) 混合实现,让个人与中小团队用一台服务器、一个脚本,拥有「安全入口 + 统一鉴权 + 数据库 + AI 审核 + 业务功能」的完整能力,免去 Kubernetes 等重型基础设施的复杂度。
### 1.2 设计哲学与核心主张
- **能力拆分但重量极轻**:保留微服务"拆分、统一入口、安全分层、可观测"的真正收益,砍掉服务注册发现、容器编排、消息总线等重型组件。
- **你完全掌控**:数据自托管、代码 MIT 开源,不绑定任何云厂商。
- **多语言自由组合**:核心用 C++(性能/系统),胶水与业务用 Python(效率),边缘网关用 Go(coraza 生态),硬件控制用仓颉(树莓派)。
- **进程托管式编排**:一个 `core` 进程当"保姆",读 `init.txt` 拉起并守护所有子服务。
### 1.3 适用对象
| 群体 | 诉求 | Blackbird 的契合点 |
|------|------|-------------------|
| 独立开发者 / 个人站长 | 不想被运维压垮 | 一键脚本 + systemd,对外只记一个域名 |
| 中小团队 | 可控、低成本内部中台 | 不依赖外部 SaaS,数据握在自己手里 |
| 注重数据主权者 | 数据绝不能出本机 | 全程自托管,MIT 可改 |
| AI / 硬件爱好者 | 想玩智能体、嵌入式舵机 | aibox 智能体 + bot4cj 舵机 + lci 帧缓冲界面 |
### 1.4 与云原生微服务的差异
未引入服务注册发现、容器编排、消息总线;服务地址在 `waf/config.yaml` 与 `gateway` 配置中静态配置。对于个人/中小规模 DIY 场景,这种取舍在简洁性与可控性上合理,但也意味着横向扩展需自行演进。
### 1.5 许可证与归属
各模块均采用 **MIT License**(详见各目录 `LICENSE`)。项目由个人开发者「qing」维护,仓库地址 `https://git.cancerai.top/qing/blackbird`。
---
## 2. 架构总览
### 2.1 UniServe 架构定义
以 `core`(C++20)为统一宿主进程,读取 `init.txt` 并经 shell 拉起多语言微服务。各服务通过内部 HTTP 对外暴露业务能力,通过 `pymsg/TCP`(自研 RPC)做数据访问与日志汇聚。
### 2.2 当前拓扑(含 ca 认证前置化)
```
┌──────────────────────────────────────────────┐
│ core (PMC / Nidus) │
│ 进程托管服务 · 监听 :5200 (HTTP 管理接口) │
│ 读取 init.txt 拉起并守护下列子进程 │
└───────────────┬──────────────────────────────┘
│ 启动 / 守护
┌──────────────┬──────────────┼──────────────┬──────────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼
┌──────────────┐ ┌─────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ redirect80 │ │ waf │ │ ca │ │ gateway │ │ fsys │ │ dnfs │
│ :80 重定向 │ │ :443 入口│ │ :8443 认证│ │ :8445 纯转发 │ │ :9200 前端 │ │ :8006 树状 │
└──────────────┘ └────┬────┘ └────┬─────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ TLS终止 │ 鉴权+前置 │ 反向代理 │ 代理笔记 │ pymsg
│ coraza │ 转发 │ (loopback) │ │
└──────────┴─────►───────┴──────────────────┴─────────────────┘
认证链路(v0.9.0 起): waf:443 ──HTTP──► ca:8443 (认证+前置转发) ──HTTP──► gateway:8445 (纯转发, 仅 loopback) ──► 内部服务
日志链路: 各服务 ──(log_client, pymsg)──► db:8005 (Logs 表,fail-safe 静默丢弃)
配置链路: 业务服务 ──(pymsg get)──► confs:8010(mtime 自动重载)
```
> `db2` / `dnfs2` 分别是 `db` / `dnfs` 的 C++20 重实现,架构位置与 Python 版一致,接口兼容可平替。`BotOps`、`lci`、`bot4cj` 不在主图中(终端前端 / 帧缓冲界面 / 树莓派硬件,不联网)。
### 2.3 core 进程托管模型
- 可执行文件 `nidus.out`,以 systemd 单元 `Nidus.service` 常驻(崩溃守护由 systemd 负责,core 自身不含自动重启逻辑)。
- 两种模式:命令行子系统(`--send/--exec/--list/--kill`)与 HTTP 伺服器(`--serve <port>`)。
- 经命名管道(Pipe)与消息队列(Mq)实现进程间通信;读 `init.txt` 自启动列表,按行拉起 ai_review / db / dnfs / smtp / static / waf / ca / gateway / captcha / aibox。
### 2.4 请求链路(8 步)
1. 客户端以 HTTP 访问 `:80`,由 `redirect80` 重定向到 HTTPS `:443`(GET/HEAD 301,其余 308 保留 body)。
2. 所有请求先打到 `waf:443`,终止 TLS、运行 coraza 规则,命中即 403。
3. 未命中请求以明文 HTTP 转发到 `ca:8443`,透传 `X-Forwarded-For` / `X-Forwarded-Proto`。
4. `ca` 对 `/login`、`/register` 做账号鉴权(PBKDF2 + RSA Token + 可选 CAPTCHA + 速率限制;注册经 pymsg/TCP 调 `smtp:8008` 发码),用户数据走 `db:8005`。
5. 其余请求携带 Token,经 `ca` 验签后注入 `X-Auth-User` 转发到 `gateway:8445`(仅 loopback,仅做存在性检查),再由 gateway 反代到 `Proxy.opened` 放开的内部服务(`fsys:9200` / `dnfs:8006`)。
6. `db:8005` 与 `ai_review:8007` 在 `Proxy.closed` 中,仅允许内部调用。
7. `dnfs` 处理节点 CRUD 时经 pymsg/TCP 调 `db` 持久化,新增节点前调 `ai_review` 审核。
8. 各服务经 `log_client.py` 异步批量推送日志到 `db` 的 `Logs` 表(fail-safe)。
### 2.5 端口一览
| 端口 | 服务 | 绑定 | 说明 |
|------|------|------|------|
| 80 | redirect80 | 0.0.0.0 | HTTP→HTTPS 重定向 |
| 443 | waf | 0.0.0.0 | HTTPS 对外入口(TLS + coraza) |
| 8443 | ca | 127.0.0.1 | 中央认证(内部 HTTP) |
| 8445 | gateway | 127.0.0.1 | 纯转发反代(loopback) |
| 5200 | core | 127.0.0.1 | PMC HTTP 管理接口 |
| 9200 | fsys | 127.0.0.1 | 前端托管(经 gateway 代理) |
| 8006 | dnfs/dnfs2 | 127.0.0.1 | 树状菜单(经 gateway 代理) |
| 8005 | db/db2 | 127.0.0.1 | 数据库服务(pymsg/TCP) |
| 8007 | ai_review | 127.0.0.1 | AI 审核(仅内部调用) |
| 8009 | captcha | 127.0.0.1 | 验证码(经 gateway 代理) |
| 8008 | smtp | 127.0.0.1 | 邮件服务(仅内部调用) |
| 8010 | confs | 127.0.0.1 | 统一配置(pymsg/TCP) |
| 8765 | aibox | 127.0.0.1 | AI 智能体(WebSocket,独立产品) |
---
## 3. 模块清单(按职责分组)
### 3.1 核心与编排
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **core** | C++20 | 5200 | 进程托管服务 PMC/Nidus,读 `init.txt` 拉起守护子进程,提供 HTTP 管理接口(`--send/--exec/--list/--kill/--serve`) |
核心设计:systemd 常驻、root 运行(见第 5 章风险)、Pipe/Mq 进程通信、无内置看门狗(崩溃自愈依赖 systemd)。
### 3.2 入口与网关层
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **redirect80** | Python | 80 | HTTP→HTTPS 重定向(GET/HEAD 301 / 其余 308),纯标准库 |
| **waf** | Go | 443 | 唯一对外入口,终止 TLS + coraza 规则匹配(SecLang)+ 反代到 ca;`kill -HUP` 热加载;规则库 `waf/rules/` |
| **ca** | C++20 | 8443 | 中央认证服务(v1.0.0,内部 HTTP),注册/登录鉴权、Token 签发校验、身份头注入后前置转发到 gateway |
| **gateway** | C++20 | 8445 | 应用层网关(v0.9.0,仅 loopback),纯转发反代,不验签(认证已迁至 ca) |
**ca 关键设计**:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节盐)、常数时间比较防 timing attack;Token 为 `Base64(payload)+"_"+RSA-Sign(payload)`,默认 1 小时过期,RSA 密钥启动时内存生成(重启即失效);`HttpOnly+Secure+SameSite=Lax` 的 `q4_token` Cookie;登录 5 次/5 分钟、注册 3 次/10 分钟速率限制;首位注册者即管理员(无默认管理员账号)。
**gateway 关键点**:纯转发,存在性不变量——显式 `Target-Url` 请求必须携带 ca 注入的 `X-Auth-User`,缺失一律 401;`Target-Url` 必须在 `Proxy.opened` 白名单。
### 3.3 业务服务(对外,经 gateway 代理)
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **fsys**(原 static) | Python/Flask | 9200 | 前端托管:社区首页/静态页/媒体流/文件浏览;**2026-08 由 static 重命名为 fsys**(core/init.txt 已同步),过渡期仍保留 legacy `static/` 目录 |
| **dnfs** | Python/Flask | 8006 | 树状菜单(Dendrachy Network File-System),基于 pid 的节点树 CRUD,新增节点前调 ai_review 审核 |
| **dnfs2** | C++20 | 8006 | dnfs 的 C++20 重实现(libhttp + libmsg),接口完全兼容,修复 P0/P1(fail-closed 验签/所有权校验/脱敏) |
| **ai_review** | Python/Flask | 8007 | AI 内容审核,DeepSeek + ChromaDB RAG(色情/违禁药/赌博/诈骗/仇恨暴力),fail-closed 宁可误拦 |
### 3.4 基础设施 / 支撑服务(仅内部调用)
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **db** | Python/pymsg | 8005 | 通用数据库服务,SQL-over-TCP(SQLAlchemy Core 参数化防注入),内置集中式日志(Logs 表 + 自动清理) |
| **db2** | C++20 | 8005 | db 的 C++20 重实现(libmsg + boost::json + libsqlite3),`execute` 路由/where 语法/响应格式对齐,可平替 |
| **confs** | Python/pymsg | 8010 | 统一配置分发,集中存放各服务配置(`confs/configs/`),内存缓存 + mtime 自动重载,路径遍历防护 |
| **smtp** | Python/pymsg | 8008 | 邮件服务,注册邮箱验证码后端(send_email/verify_email/health),6 位码内存存储、冷却/限流/熔断 |
| **captcha** | Python/pymsg | 8009 | 验证码服务,图形验证码(一次性、5 分钟过期),经 ca 的 `GET /captcha/image` 代理给浏览器 |
### 3.5 AI 智能体与前端
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **aibox** | Python | 8765 | 独立 AI 智能体系统(DeepSeek + WebSocket),自带技能系统/记忆/任务调度/前端与语音客户端;由 core 拉起但与主干微服务无调用关系 |
| **BotOps** | Python/Textual | — | aibox 的终端 TUI 前端,WebSocket 连接 aibox,Markdown 增量流式渲染、思考模式、任务侧栏、自动重连 |
| **actpred** | Python/PyTorch | — | 动作预测(图像 + 动作历史 → 预测下一个动作),bot4cj 的「大脑」配套;支持从 `.mt` 脚本预训练 |
### 3.6 机器人硬件与嵌入式
| 模块 | 语言 | 端口 | 职责 |
|------|------|------|------|
| **bot4cj** | 仓颉/Cangjie | — (树莓派 I2C) | 机器人硬件脚本解析器,解析 `.mt` 驱动 PCA9685 舵机 / MPU6050 陀螺仪 / 扬声器,面向 Dashun 蜘蛛机器人 |
| **lci** | C++ | — (/dev/fb0) | 嵌入式帧缓冲图形界面(无需 X11/Wayland),矢量 GB2312 中文字库、图片渲染、实时数据可视化 |
| **3d** | — (资产) | — | 机器人 / 机械臂 CAD 模型(STEP/STL/SLDPRT),bot4cj 配套硬件资产 |
### 3.7 共享基础库
| 库 | 语言 | 说明 |
|----|------|------|
| **libth** | C++ | 线程库:`Thread`、`Cv_wait`、`Fsm`、`ITask` |
| **libhttp** | C++ | HTTP/WebSocket 库(Boost.Beast):`HttpServer`(含 WS 服务/代理)、`HttpClient`(含 WS 客户端) |
| **libcrypt** | C++ | 加密库:`AES_CBC`、`MyRSA`、`SHA256`、`Base64`、`Crypto_Basic` |
| **libsqlite3** | C++ | SQLite3 header-only 封装 |
| **libmsg** | C++ | pymsg 协议 C++ 实现(4B 大端长度前缀 + UTF-8 载荷),`MsgClient/Server`、`RpcClient/Server`、`AsyncQueue`,与 Python pymsg 字节级兼容 |
| **libtry** | C | 嵌入式脚本解释器(~741 行),手写递归下降,支持中文标识符,用于嵌入式操作界面 shell |
| **pymsg** | Python | 自研 RPC 基础库(`[4B 长度][UTF-8 JSON]`),db 服务端与各 `log_client.py` 的基础依赖(setuptools 包) |
| **memdb** | C++ | 内存数据库(internal 组件,未在主 README 模块表列出) |
### 3.8 测试与文档
| 项 | 说明 |
|----|------|
| **tests** | 黑盒测试套件(`test_db_*`/`test_gateway_*`/`test_waf_*`/`test_dnfs_*`),子进程 + HTTP 验证,前后自动备份恢复 db |
| **docs** | 文档聚合(security/ 审计、reviews/ 审查、reports/ 分析、INDEX.md),41+ 篇 md |
---
## 4. 通信协议:pymsg / libmsg
这是 Blackbird 异构架构的通信基石。
- **帧结构**:`[4 字节大端长度][UTF-8 JSON]`,单帧上限 **64 MiB**。
- **跨语言一致**:Python `pymsg` 与 C++ `libmsg` 字节级兼容,是 v1/v2 双栈迁移的关键支撑。
- **资源防护**:慢连接总 deadline 防护 + 有界线程池,控制资源耗尽类风险。
- **错误脱敏**:`_sanitize_error` 以正则替换绝对路径,降低信息泄露面。
- **安全现状(重要)**:协议**无握手、无鉴权、明文无 TLS**,安全性完全依赖 `127.0.0.1` 主机隔离。一旦内网隔离被突破(同机多租户或某内部服务失陷),所有跨进程调用即暴露。详见第 5 章与加固计划。
---
## 5. 安全体系
### 5.1 边界鉴权优势(已达 A-)
gateway(及 ca)的边界鉴权设计扎实,安全评级由 **B+ 提升至 A-**(Gateway v0.8.0 修复 P0:错误消息脱敏、SIGTERM 退出、doAccept 无限递归、8 秒关闭超时):
- 口令:PBKDF2-HMAC-SHA256(10 万轮 + 16 字节盐),常数时间比较防时序攻击。
- Token:RSA-2048 + PKCS#1 v1.5 签名,双轨吊销(jti 黑名单 + 用户版本号)。
- 多层防护:CAPTCHA 一次性、登录 5/300s、注册 3/600s、邮箱级双窗口限流。
- Cookie:`HttpOnly;Secure;SameSite=Lax`。
- 管理面 fail-closed:`core` HTTP 接口绑 127.0.0.1 且 `verify_admin_token`。
### 5.2 已核实缺口(来自 2026-08-08 研究报告的审计结论)
> 以下结论基于研究报告对当时代码快照的核实,整理时(2026-08-10)部分项已在 `ca` 引入、C++ 重写潮与加固计划中进入整改。**具体是否已在最新提交修复,需逐文件人工复核,状态标记为 `human_review_pending`。**
| 级别 | 缺口 | 说明 | 关联整改 |
|------|------|------|----------|
| 高危 1 | WAF 规则失效 | `waf/rules/local/custom.conf` 的 6 条 SecRule 全部被注释,无 `crs/` 目录 → WAF 仅做 TLS 终止 + 响应头,不拦截 SQLi/XSS/命令注入 | 加固计划阶段一目标;待启用规则 |
| 高危 2 | core MQ 未鉴权即 exec | `parse_data_from_mq` 对 list/kill/exec 未鉴权即经 `/bin/sh -c` 执行;core 以 root 运行 → 本地 root RCE | 加固计划阶段二核心项 |
| 高危 3 | 内部服务零信任缺失 | pymsg 无认证;db 的 `execute` 路由接受任意 action/table/where,仅依赖 127.0.0.1 隔离 → 边界失守后可免认证读写全库 | 加固计划阶段三(AES-GCM) |
| 中危 1 | ai_review 未鉴权 + 提示注入 | `/review` 仅 per-IP 限流、无鉴权;用户输入直接拼入系统提示词 → 审核绕过 + 开放 LLM 端点 | aibox_open_dev_plan 跟踪 |
| 中危 2 | 密钥配置缺陷 | `.env.example` 写 `OPENAI_API_KEY` 而代码读 `DEEPSEEK_API_KEY` → 变量名不一致的静默失效 bug | 待对齐 |
### 5.3 安全加固计划(三层防线)
`docs/security/SECURITY_HARDENING_PLAN_20260803.md` 定义了"隔离层 + 认证层 + 加密层"三阶段目标:
- **阶段一(隔离加固,P0)**:所有内部服务绑 127.0.0.1(✅ 已实现 db/confs/captcha/gateway/static/smtp/dnfs);防火墙仅放行 443、外网端口扫描验证仍 `[ ]` 待办。
- **阶段二(core 密钥注入,P1)**:core 启动时生成进程组密钥,经 `execve` 的 envp 注入 `MICROSERV_KEY`,子进程 fail-fast 读取——仍 `[ ]` 待办。
- **阶段三(AES-GCM 通信加密,P1)**:编写 `microcrypt` 库,接入 pymsg 帧与 HTTP 调用,防篡改/重放——仍 `[ ]` 待办。
### 5.4 安全审计覆盖(`docs/security/`,41+ 篇)
包含全局审计(DEPLOY / DEPS / SECURITY_HARDENING_PLAN)与逐模块审计报告(waf、gateway×3、core、db、db2、pymsg、dnfs、dnfs2、static/fsys、ai_review、smtp、smtp3、captcha、captcha2、confs、libmsg、libhttp、libsqlite3、libth、aibox、BotOps、actpred)。`docs/reviews/` 另有 4 篇代码审查日志。可审计性远超多数同类项目,是风险闭环的基底。
---
## 6. 部署与运维
### 6.1 依赖
- **系统级**:`g++`(C++20)、`make`、`libboost-all-dev`、`libcurl4-gnutls-dev`、`libssl-dev`、`libsqlite3-dev`、`python3` + venv + pip、`patchelf`、`golang-go`(Go 1.22+,用于 waf)、`-ljpeg -lpng`(lci)。
- **Python**:Flask、Flask-CORS、Flask-SQLAlchemy、SQLAlchemy、waitress、openai、chromadb、python-dotenv、nuitka。
- **Go**:corazawaf/coraza v3.3.0、gopkg.in/yaml.v3。
### 6.2 安装链(一键脚本)
```bash
# 1. 系统依赖
bash _setup.deb.sh # Debian/Ubuntu
# 或
bash _setup.rhel.sh # RHEL/CentOS/Rocky/Fedora
# 2. 依次构建安装(库先行,core 最后)
bash _inst.sh
```
单独安装某服务:`bash _set.sh`(构建+安装)、`bash _run.sh`(运行)。部署路径约定 `/opt/micro_servers/`。
### 6.3 运行
`init.txt` 自启动顺序重要(redirect80 → waf → gateway → …)。启动 core:
```bash
systemctl start Nidus # 或 bash core/_run.sh
```
对外入口:`https://<host>:443`(waf 终止 TLS 后转发)。
### 6.4 WAF 与 gateway 协作
启用 WAF 后证书从 gateway 迁到 waf:`cp gateway/full_chain.crt gateway/server.key waf/certs/`。gateway 改内部 HTTP 模式(`tls=off`、`address=127.0.0.1`、`port=8443`→现 8445)。独立部署(不用 WAF)时 gateway 自行 `tls=on`、`port=443`、`address=0.0.0.0`。
### 6.5 容器化现状
`docker-compose.yml` 的 services 块**仅纳入 db / ai_review / dnfs 三个服务**(另含 networks 与 volumes),core/waf/gateway/fsys/confs/captcha/smtp 未纳入 → 编排不完整。生产部署当前以 systemd 链路为准。
---
## 7. 测试与质量
- **黑盒测试**:`tests/blackbox/` 约 8 个集成测试,覆盖 db / dnfs / gateway / waf / static / captcha / smtp / ai_review;无单元测试、无 CI(仓库无 `.github/workflows`)。
- **文档/审计完备度**:`docs/security/` 24+ 份逐模块审计 + `docs/reviews/` 评审日志,可审计性极高。
- **双实现(v1/v2)重构负担**:db↔db2、dnfs↔dnfs2、pymsg↔libmsg、libtry↔libth、lci↔bot4cj 并存,正以「C++ 重写 Python 微服务」为目标迁移。任一逻辑修正需两语言同步,存在语义漂移风险——建议重构收口时优先合并单轨。
---
## 8. 开发路线图与近期重点(2026-08)
### 8.1 C++ 重写潮(2026-08-09 引入)
| 新增/重写模块 | 说明 |
|--------------|------|
| **ca** | 中央认证服务(C++ 2463 行),认证独立化,接管原 gateway 端口前置鉴权 |
| **captcha2** | C++ 重写 captcha(含 stb_image_write),修复 P0 大小写缺陷 |
| **confs2** | C++ 重写 confs(libmsg RpcServer 替代 pymsg),协议兼容零改动 |
| **smtp3** | C++ 重写 smtp(1013 行,SMTP_SSL 手写),审计 7 项遗留一并修复 |
| **core ConfsTask** | C++ 内嵌配置任务(557 行) |
| **gateway 精简** | `src/main.cpp` 大幅精简(-1980 行),认证逻辑下沉至 ca |
### 8.2 认证独立化(auth_service_plan)
`waf → ca → gateway` 前置拓扑:ca 统一鉴权并前置转发,gateway 退化为纯转发(仅 loopback),下游 / WAF / 前端零改动。对应开发文档见 `docs/reports/auth_service_plan.md`。
### 8.3 关键 dev docs 索引(`docs/reports/`)
core_service_registry_dev_doc(注册中心)、auth_service_plan(CA 模块)、aibox_open_dev_plan / aibox_file_network_cli_refactor(aibox 安全沙箱与 CLI 化)、libcrypt_CBC_deprecation_dev_doc(AES-GCM 迁移)、libhttp_remote_addr_dev_doc(X-Remote-Addr 透传)、captcha_cpp_rewrite / smtp_cpp_rewrite / confs2_cpp_rewrite(C++ 重写方案)、actpred_pretrain_test(预训练与推理测试)。
### 8.4 安全评级演进
Gateway v0.8.0 修复 P0 后,**安全评级 B+ → A-**;随 ca 引入与 C++ 重写潮加固,整体向"端到端一致鉴权"收敛(详见第 5 章待办)。
---
## 9. 许可证与推广
- **许可证**:各模块 MIT。
- **价值主张摘要**(详见 `docs/blackbird_promo_pack.md`):
- "微服务的好处,Kubernetes 的麻烦,二选一?Blackbird 说:都要,但只留好处。"
- "你的数据、你的服务、你的规则——一台机器,全部自己说了算。"
- Slogan 候选:*轻装微服务,主权握手中*;*一台服务器,一座可控的城*;*微服务的纯粹主义*。
---
## 10. 风险与待办(收口)
| 风险 / 待办 | 级别 | 状态 | 来源 |
|------------|------|------|------|
| WAF 检测规则未启用 | 高危 | `human_review_pending`(计划阶段一目标) | 研究报告 / 加固计划 |
| core MQ 未鉴权 RCE(root 运行) | 高危 | `human_review_pending`(计划阶段二) | 研究报告 / 加固计划 |
| 内部 pymsg/db 零信任缺失 | 高危 | `human_review_pending`(计划阶段三) | 研究报告 / 加固计划 |
| ai_review 未鉴权 + 提示注入 | 中危 | `human_review_pending` | 研究报告 / aibox_open_dev_plan |
| 密钥变量名不一致(DEEPSEEK/OPENAI) | 中危 | `advisory`(待对齐) | 研究报告 |
| docker-compose 仅 3 服务,不完整 | P2 | `advisory` | 研究报告 |
| 无单元测试 / 无 CI | P2 | `advisory` | 研究报告 |
| v1/v2 双实现维护成本 | 结构性 | `advisory`(建议收口合并) | 研究报告 |
| 文档口径差异(MODULES 记 gateway:8443) | 低 | `advisory`(以 README 为准) | 本文档核实 |
### 建议交付清单
- [ ] 启用 WAF 规则并补建 `crs/`(直击高危 1)
- [ ] 为 core MQ 通道落地 `verify_admin_token` 并降权运行(直击高危 2)
- [ ] 内部服务加调用方鉴权 / pymsg 上 mTLS 或 Unix socket(直击高危 3)
- [ ] ai_review `/review` 加鉴权 + 结构化提示词拼接
- [ ] 统一 `DEEPSEEK_API_KEY` 与 `.env.example`
- [ ] 补全 compose 或文档声明 systemd 为唯一受支持链路
- [ ] 引入单元测试与 CI,降低双实现回归风险
---
## 续接胶囊
- **文档类型**:项目综合资料长文档(Blackbird / micro_servers)
- **目标读者**:开发者、维护者、安全审计者、潜在贡献者
- **核心目标**:将分散在 40+ 篇仓库文档中的架构、模块、协议、安全、部署、路线图整合为单一可查阅手册
- **已完成章节**:第 0–10 章(概述、架构、模块清单、pymsg 协议、安全体系、部署运维、测试质量、路线图、许可证、风险收口)
- **已确认事实**:主 README 拓扑(waf:443→ca:8443→gateway:8445);ca 为 C++ 认证前置模块;C++ 重写潮(ca/captcha2/confs2/smtp3/gateway 精简/core ConfsTask);安全评级 B+→A-;MIT 许可
- **待核验事项**:3 项高危 + 2 项中危是否已在最新提交修复(研究报基于 2026-08-08 快照);docker-compose 补全状态;MODULES.md 端口口径同步
- **下一节唯一目标**:(可选)补充"各模块 API 速查表"或"从零部署实战 walkthrough"
- **续写口令**:`继续编辑 docs/blackbird_project_profile.md,当前停于第 10 章风险收口;已含架构/模块/协议/安全/部署/路线图;约束:以主 README 拓扑为准、高危缺口标注 human_review_pending、不虚构修复状态;下一目标:补模块 API 速查表或部署实战。`
@@ -0,0 +1,141 @@
# fsys/public 前端代码研究报告
> 日期:2026-08-10 · 范围:`fsys/public/` 全部前端代码(**不含 `games/` 目录**) · 托管方:fsys(C++20,127.0.0.1:9200)
---
## 一、总览
fsys/public 是 UniServe 框架对外的全部 Web 前端,由 fsys 静态托管,经 `waf:443 → ca:8443 → gateway:8445 → fsys:9200` 链路送达浏览器。整套前端为**零构建、零框架的原生 HTML/CSS/JS**(Vanilla 技术栈),无打包器、无 npm 依赖,第三方库仅两个 vendored 文件(markdown-it 12.0.0、highlight.js 11.9.0)。
### 代码规模(不含 games/)
| 类别 | 文件 | 行数 | 说明 |
|------|------|------|------|
| 自研 JS | 8 个 | ~2,650 | 业务逻辑主体 |
| 第三方 JS | 2 个 | ~9,500 | markdown-it / highlight.js(vendored,不分析) |
| CSS | 8 个 | ~2,370 | 含设计令牌体系 theme.css |
| HTML | 13 个 | ~2,100 | 含 allcolor.html 内联数据 ~400 行 |
| 合计(自研) | 26 个 | **~7,100** | |
### 页面地图
```
public/
├── index.html 社区首页( landing,Hero + 特性 + 关于 + CTA )
├── login.html 登录(图形验证码 + 用户条例弹窗 + 第三方 yhdet 快捷登录)
├── register.html 注册(图形验证码 + 邮箱验证码双因子)
├── forgot.html 重置密码(同注册双因子流程,脚本内联)
├── dnfs.html 树状笔记主应用(核心 SPA 式单页)
├── aibox.html aibox 智能体 Web 客户端(WebSocket,仅管理员)
├── __view.html 目录浏览页(fsys 无 index.html 时的回退页,__ 前缀被 filelist 过滤)
├── allcolor.html 中国传统色彩调色板(纯静态工具页,内联数据)
├── LICENSE.html 用户使用条例(静态文本,供登录页 iframe 弹窗复用)
├── mgmt/ 进程管理器(管理员后台,iframe 嵌套 features/*.html)
│ ├── index.html 旧版独立登录页(疑似遗留,见 §六问题 P1)
│ ├── mgmt.html 管理主框架(左侧功能列表 + iframe 加载功能页)
│ └── features/ process(可用) / user / module / log(占位)
├── css/ theme / index / auth / dnfs / view / aibox / codeblock(空)
└── js/ token_manager / validation / index / login / register / dnfs / aibox / view
```
---
## 二、认证体系:HttpOnly Cookie + Token 生命周期管理
整套前端已在 2026-08 完成向 **HttpOnly Cookie(q4_token)** 的迁移,JS 全程不经手 Token,这是本次研究中最重要的一条架构主线。
### 2.1 token_manager.js(119 行,全局基础设施)
被 index / dnfs / aibox 三页引用,做两件事:
1. **定时静默刷新**:通过 `POST /whoami` 取 `created_at + keep_time` 估算过期点(注释明确指出不能用 `iat`——刷新后 iat 不变),在 80% 生命周期处调 `POST /refresh_token`;失败进入 15s 冷却重估,两次刷新最小间隔 60s。
2. **全局 fetch 拦截器**:覆写 `window.fetch`,任何请求收到 401 先自动刷新一次 Token 再重试原请求;`/refresh_token` 自身排除防循环。Cookie 由 gateway `Set-Cookie` 自动更新,重试无需改请求头。
### 2.2 各页登录态处理
| 页面 | 检测方式 | 401 处理 |
|------|----------|----------|
| index | `POST /whoami`,已登录则切换头部按钮组与 CTA 文案 | 静默忽略(首页允许匿名) |
| dnfs | `loadUserPanel()` 拉 `/whoami`,按 `is_admin` 显示特殊节点面板与管理入口 | 仅笔记接口 401 跳 login.html,其他错误原地提示(不再误判凭证失效) |
| aibox | 启动预检 `/whoami`,非管理员仅提示不阻塞 | WS 握手由 gateway 强制 is_admin 兜底 |
| mgmt | `mgmt.html` 内联 `loadUserInfo()`,未登录 1s 后跳 `index.html` | features/process.html 遇 401 弹窗后跳登录 |
### 2.3 注册/登录/重置三流程
- **login**:邮箱 + 密码 + 图形验证码(`GET /captcha/image`,token 走 `X-Captcha-Token` 响应头,图片 blob → ObjectURL,旧 URL 主动 revoke)。无论成败提交后强制刷新验证码(token 一次性消费)。另有 yhdet 开源社区第三方快捷登录入口(外跳 callback)。
- **register**:图形验证码 → `POST /register/send_code` 发 6 位邮箱验证码(10 分钟有效,前端 60s 重发倒计时与服务端冷却对齐,HTTP 429 时也启动倒计时)→ `POST /register` 提交。
- **forgot**:与 register 几乎同构,走 `/reset/send_code` + `/reset_password`;脚本**直接内联在 HTML 里**(339 行中约 290 行是 register.js 的复制改造)。
- **WafTip**(validation.js):统一识别 WAF 拦截特征(403 + 纯文本 `blocked by WAF`),与 gateway 自身的 JSON 格式 403 区分,三处表单均接入。
---
## 三、dnfs 树状笔记(dnfs.html + dnfs.js 1187 行,核心应用)
### 3.1 导航模型
- 用 `stack` 数组记录节点路径(节点 id 栈,根为 0),点卡片标题 push、返回 pop,配合 `getNode(id)` 拉子节点列表。
- `Target-Url: http://127.0.0.1:8006` 请求头由 gateway 转发到 dnfs2。
- **导航守卫** `confirmLeaveIfDirty()`:表单有未提交内容时拦截导航(明确注释强调必须在改 stack 之前调用,否则栈错位)。
- 右悬浮 **minimap**(当前层节点锚点跳转 + 折叠),左悬浮 **userpanel**(用户名/角色/特效开关/管理入口/退出),窄屏(<768px)默认折叠。
### 3.2 内容渲染管线
- 普通节点:markdown-it(`html: false`)+ highlight.js 代码高亮。
- **特殊节点**(管理员专属,信封格式 `{"_sp":"link|js|image|file", ...}`):
- `link`:favicon 经服务端代理 `/api/favicon?url=`(SSRF 防护在 fsys 侧),新标签 `noopener noreferrer`。
- `js`:可审计/可运行的前端特效,**iframe `sandbox="allow-scripts"` + srcdoc** 隔离执行,不用 eval/new Function(CSP 未开 unsafe-eval);`<!-- in-node -->` 标记决定挂在卡片内还是 fixed 全屏(注释详细论证了层叠上下文陷阱);用户面板提供"无特效模式"开关(localStorage 持久化)。
- `image`:DataURL 直显;`file`:base64 → Blob 触发下载;上传上限 2.5MB(与后端 3.5M base64 字符上限对齐)。
- 权限显隐:发布权限后端强制(is_admin),前端仅按 `can_operate` 字段渲染编辑/移动/删除按钮组。
### 3.3 其他特性
- 当前层实时搜索(标题/owner/内容,信封类排除 base64),ESC 清空,无匹配有提示。
- 节点移动 = pid 变更的 update_note,底部 movebar 二次确认;移动/发布会走后端 AI 审核,期间按钮组禁用防重。
- 互动统计图标(观看/点赞/分享)已就位,但后端 Notes 表无计数字段,恒显示 0(预留)。
- 大量防御性注释记录了历史 bug 修复(空栈取 `stack[-1]` 得 undefined、confirm 取消时 Promise 兜底恢复按钮等)。
---
## 四、aibox Web 客户端(aibox.html + aibox.js 435 行)
- **WebSocket** 经 gateway `[WsProxy]` 反代至 aibox 服务(`/ws/aibox`),协议与 BotOps/aibox `client.py` 完全兼容。
- 完整流式渲染:chunk 增量拼接 + markdown-it 渲染;reasoning 思考区可折叠、结束后默认折叠、超长仅保留末尾 3000 字符;`ai_break` 支持工具调用分段气泡;中断(interrupt)有完整状态收尾。
- 连接韧性:指数退避重连(2s→30s 封顶)、10s 心跳(低于前置代理 proxy_read_timeout)、手动断开抑制重连。
- 任务面板为抽屉式覆盖层,未读角标(99+ 封顶);本地命令 `/clear /help /connect /disconnect /interrupt`,其余 `/` 命令透传服务端。
- 移动端细节:`enterkeyhint="send"`、`isComposing` 防中文输入法误发、viewport-fit=cover 刘海屏适配。
## 五、__view 目录浏览(__view.html + view.js)
fsys 对无 index.html 目录的回退浏览页(`__` 前缀使其被 `/api/filelist` 过滤,不出现在列表里)。面包屑 + 搜索(`/` 或 Ctrl+F 聚焦、200ms 防抖、ESC 清空)+ 分类统计(目录/文件/视频/音频)+ 网格列表;视频音频跳 `/video/`(fsys Range 206 流式),`?download=true` 触发下载。所有文件名经 `esc()` 转义防 XSS。
## 六、mgmt 进程管理器(管理员后台)
- `mgmt.html`:顶栏用户信息 + 左侧功能列表 + iframe 加载功能页;功能列表中日志页已被注释停用。
- `features/process.html`:**唯一真正可用的功能页**,经 gateway 代理 core:5200,实现进程列表(兼容 boost property_tree 把数组序列化成对象的怪癖)、新建(/exec)、停止(/kill)、401 跳登录。
- `features/user.html` / `module.html` / `log.html`:占位静态页,按钮仅 alert"需要后端 API 支持"。
## 七、CSS 设计体系
- **theme.css(686 行)是全站基座**:完整 Design Tokens(`--bg/--fg/--accent/--error...` 约 24 个变量),`prefers-color-scheme` 暗色全套覆写,暖棕橙品牌色(accent `#e8853a`),统一表单/按钮/提示(tip-red/yellow/green)/卡片/页脚,480px/360px 两档响应式。
- 页面级 CSS 在其上叠加:index.css(landing 各 section + 滚动渐显 + 光标光斑)、auth.css(注册/重置页品牌渐变顶条)、dnfs.css(点阵底纹、卡片逐张入场动画、`prefers-reduced-motion` 降级)、view.css / aibox.css(全屏应用布局覆盖 theme 居中卡片默认)。
- 一致性良好:动效全部尊重 `prefers-reduced-motion`,悬浮面板统一 backdrop-filter 毛玻璃,暗黑模式覆盖完整。
## 八、发现的问题与改进建议
| # | 级别 | 位置 | 问题 |
|---|------|------|------|
| P1 | 中 | mgmt/index.html + mgmt.html | **旧认证体系遗留**:自建 `/api/login` + JS 写 `token=` Cookie(非 HttpOnly q4_token),logout 删的也是 `token` Cookie,与现行认证完全脱节;该登录页应删除或改造,避免成为绕过口 |
| P2 | 中 | js/view.js | 文件名经 `esc()` HTML 转义后仍**拼进 `onclick="navigate('${path}')"` 内联字符串**:`&#39;` 在属性解析时会解码回单引号,单引号文件名可断开 JS 字符串造成注入。建议改 addEventListener + dataset |
| P3 | 低 | register.js / forgot.html | `POST /register`、`/reset_password` 声明 `Content-Type: application/x-www-form-urlencoded` 但 body 实为 JSON 字符串(后端兼容故能用,契约不洁) |
| P4 | 低 | login.js / register.js / forgot.html | refreshCaptcha / startResendCooldown / 校验三件套**复制了三份**,forgot 干脆内联 290 行;建议抽 `js/auth_common.js` |
| P5 | 低 | js/dnfs.js | 非严格模式下 `obj/jsonBody` 等多处未声明即赋值(隐式全局);`card.id = item.title` 标题重复或含特殊字符时锚点冲突 |
| P6 | 低 | 空文件 | `css/codeblock.css`(0 行,dnfs.html 仍引用)、`mgmt/features/log.css`(0 行) |
| P7 | 低 | mgmt/features/{user,module,log}.html | 占位页无实现,入口仍展示"模块/用户"按钮,建议隐藏或标注 |
| P8 | 提示 | dnfs.js 统计图标 | visit/like/share 前端已渲染但后端无字段,恒为 0 |
## 九、结论
fsys/public 前端在零框架约束下达到了相当高的完成度:**认证设计现代化**(HttpOnly Cookie + 静默刷新 + 401 拦截重试)、**安全细节扎实**(WAF 特征识别、验证码一次性消费、iframe 沙箱特效、SSRF 防护代理、favicon blob 回收)、**可访问性与移动端适配到位**(reduced-motion、enterkeyhint、isComposing、safe-area)。注释质量突出,大量历史 bug 的成因与防御约定都沉淀在代码里。
主要欠账集中在 **mgmt 子系统**(旧认证遗留 + 三个占位页)和**三处表单脚本的复制粘贴**。优先级建议:P1(旧登录页拆除)> P2(view.js 内联事件注入)> P4(公共脚本抽取)> 其余。
+102 -408
View File
@@ -10,417 +10,111 @@
## 问题严重等级
| 等级 | 说明 | 响应时间 |
|------|------|----------|
| P0 🔴 | 可导致服务崩溃、DoS 或远程接管、数据发往错误客户端 | 立即修复 |
| P1 🟠 | 可导致资源耗尽、信息泄露、拒绝服务 | 72小时内 |
| P2 🟡 | 健壮性/配置问题、校验缺失 | 一周内 |
| P3 🟢 | 优化建议 | 版本迭代 |
| 等级 | 说明 | 响应时间 |
| ----- | --------------------------- | ----- |
| P0 🔴 | 可导致服务崩溃、DoS 或远程接管、数据发往错误客户端 | 立即修复 |
| P1 🟠 | 可导致资源耗尽、信息泄露、拒绝服务 | 72小时内 |
| P2 🟡 | 健壮性/配置问题、校验缺失 | 一周内 |
| P3 🟢 | 优化建议 | 版本迭代 |
---
## 修复进度汇总
| 审计轮次 | 等级 | 总数 | 已修复 | 待修复 | 修复率 |
|----------|------|------|--------|--------|--------|
| 第一轮 (2026-08-06) | P0 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P1 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P2 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P0 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P2 | 4 | 4 | 0 | 100% |
| 第三轮 (2026-08-07) | P1 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P2 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P3 | 1 | 1 | 0 | 100% |
| **第四轮 (2026-08-07)** | **P1** | **1** | **1** | **0** | **100%** |
| **第四轮 (2026-08-07)** | **P2** | **2** | **2** | **0** | **100%** |
| **第四轮 (2026-08-07)** | **P3** | **2** | **2** | **0** | **100%** |
| **合计** | **—** | **16** | **16** | **0** | **100%** |
| 审计轮次 | 等级 | 总数 | 已修复 | 待修复 | 修复率 |
| -------------------- | ------ | ------ | ------ | ----- | -------- |
| 第一轮 (2026-08-06) | P0 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P1 | 1 | 1 | 0 | 100% |
| 第一轮 (2026-08-06) | P2 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P0 | 1 | 1 | 0 | 100% |
| 第二轮 (2026-08-07) | P2 | 4 | 4 | 0 | 100% |
| 第三轮 (2026-08-07) | P1 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P2 | 1 | 1 | 0 | 100% |
| 第三轮 (2026-08-07) | P3 | 1 | 1 | 0 | 100% |
| **第四轮 (2026-08-07)** | **P1** | **1** | **1** | **0** | **100%** |
| **第四轮 (2026-08-07)** | **P2** | **2** | **2** | **0** | **100%** |
| **第四轮 (2026-08-07)** | **P3** | **2** | **2** | **0** | **100%** |
| **合计** | **—** | **16** | **16** | **0** | **100%** |
---
## 🔴 P0 — 严重问题
## 已解决发现(16 项,100% 修复)
> 以下全部发现的根因分析、触发场景与修复说明已作为 `[libmsg-audit]` 注释合并到源码,便于在代码处直接对照审计结论。源码位置:`libmsg/inc/us/{MsgServer,Msg,Msg_Basic,RpcServer,RpcClient,AsyncQueue}.hpp`(在源码中搜索 `[libmsg-audit]` 即可定位到各修复点)。
| 发现 | 等级 | 轮次 | 源码位置 | CWE | 修复摘要 |
| ---- | ----- | --- | ------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
| P0-1 | P0 🔴 | 第一轮 | MsgServer.hpp: broadcast / reply / \_remove_client | CWE-362 | 持 per-connection send_lock 再 close,防 fd 关闭后被 OS 复用导致数据发往错误客户端 |
| P0-2 | P0 🔴 | 第二轮 | MsgServer.hpp: \_close_all_clients | CWE-362 | 批量关闭路径同样持 send_lock 后 shutdown+close,修复第一轮只覆盖单路径的竞态回归 |
| P1-1 | P1 🟠 | 第一轮 | MsgServer.hpp: \_worker_loop / \_handle_client;Msg.hpp: recvExactDeadline | CWE-400 | workers>0 时 recv 移交 worker 线程,事件循环不再阻塞,防 Slowloris DoS |
| P1-2 | P1 🟠 | 第三轮 | MsgServer.hpp: \_accept_client | CWE-400 | 新连接设置 SO_SNDTIMEO=5s,慢客户端不读数据时 send 超时抛异常,防服务级 DoS |
| P1-3 | P1 🟠 | 第四轮 | MsgServer.hpp: ConnectionLock / reply / broadcast / \_remove_client | CWE-362 | 引入 atomic active 标志,close 前设为 false,彻底消除 reply/broadcast 与 \_remove_client 的残余竞态 |
| P2-1 | P2 🟡 | 第一轮 | RpcServer.hpp: \_sanitize_error | CWE-209 | 脱敏正则扩展为 `/\S+(?:/\S*)*`,覆盖含特殊字符路径,防内部路径泄露 |
| P2-2 | P2 🟡 | 第二轮 | Msg.hpp: pmsg_recv(int) 注释警告 | CWE-400/672 | 文档标注 SO_RCVTIMEO 前置条件,推荐 deadline 模式,防误用永久阻塞(说明并入 P2-3 文档) |
| P2-3 | P2 🟡 | 第二轮 | Msg.hpp / MsgServer.hpp / Rpc*.hpp / AsyncQueue.hpp 各入口 | CWE-20 | 新增 validateTimeout / validatePort 及全量配置边界校验,拒绝非法输入 |
| P2-4 | P2 🟡 | 第二轮 | Msg.hpp: isValidUtf8 | CWE-20 | 接收路径新增完整 UTF-8 校验,与 Python decode('utf-8') 对齐 |
| P2-5 | P2 🟡 | 第二轮 | MsgServer.hpp: 构造函数 | CWE-20 | 配置项范围校验(已并入 P2-3):recv_timeout / max_connections / max_queue_size / workers / poll_timeout / listen_backlog |
| P2-6 | P2 🟡 | 第三轮 | MsgServer.hpp: reply | CWE-703 | reply 发送失败标记 \_failed,由主循环统一清理,与 broadcast 一致 |
| P2-7 | P2 🟡 | 第四轮 | MsgServer.hpp: \_server_fd | CWE-362/662 | \_server_fd 改为 std::atomic<int>,消除 stop() 与 \_event_loop 数据竞争 UB |
| P2-8 | P2 🟡 | 第四轮 | MsgServer.hpp: start | CWE-20 | start() 校验 on_message 非空,防 bad_function_call |
| P3-1 | P3 🟢 | 第三轮 | RpcClient.hpp: call | CWE-20 | 调用 as_bool() 前校验 success 为 bool,类型不符抛 RpcError |
| P3-2 | P3 🟢 | 第四轮 | RpcServer.hpp: \_dispatch | CWE-209 | \_route 缺失 vs 类型不符返回不同错误信息 |
| P3-3 | P3 🟢 | 第四轮 | MsgServer.hpp: \_accept_client | — | setsockopt 移到加入 \_clients 之前,先完成 socket 配置再暴露给其他线程 |
### P0-1: broadcast/reply 竞态条件导致数据发送到错误客户端 ✅ 已修复(第一轮)
- **CWE**: CWE-362 (Race Condition)
- **OWASP**: A01:2021
- **位置**: `MsgServer.hpp:228`(原始行号)
- **置信度**: 80%
**问题描述**:
`broadcast()` 在 `_clients_mtx` 保护下拷贝 fd + send_lock 列表后释放锁,再无锁执行 `pmsg_send(fd, msg)`。此窗口期内 `_remove_client()` 可关闭该 fd,OS 可将其分配给新连接。`broadcast()`/`reply()` 随后向复用的 fd 发送数据,导致消息到达错误的客户端。`_remove_client()` 原实现关闭 fd 前不获取 per-connection `send_lock`,因此 `send_lock` 无法阻止此竞态。
**修复方案**(commit `e92ef05`):
`_remove_client()` 先从 `_send_locks` 取出 `send_lock` 并从 map 删除,持有 `send_lock` 后再执行 `::close(fd)`,确保关闭与正在进行的发送操作互斥。
---
### P0-2: _close_all_clients() 批量关闭路径存在同类竞态回归 ✅ 已修复(第二轮)
- **CWE**: CWE-362 (Race Condition)
- **OWASP**: A01:2021
- **位置**: `MsgServer.hpp::_close_all_clients()`(析构/stop路径)
- **置信度**: 90%
**问题描述**:
第一轮修复只覆盖了单客户端移除路径 `_remove_client()`,但批量关闭路径 `_close_all_clients()`(在 `start()` 异常退出或正常退出时调用)直接遍历 `_clients` 并 `::close(fd)`,**未持有每连接 `send_lock`**。在竞态窗口内,reply/broadcast 可能在 `_clients_mtx` 释放后、`close(fd)` 执行前获取到 send_lock,或 `close(fd)` 后 OS 复用 fd 给新连接,导致数据发送到错误客户端。
**触发场景**:
1. 服务调用 `stop()` 或 `start()` 异常退出,进入 `_close_all_clients()`
2. 另一线程正在调用 `reply()`/`broadcast()`,已在 `_clients_mtx` 下拷贝了 send_lock 但尚未持有 send_lock 发送数据
3. `_close_all_clients()` 无锁直接 close(fd),OS 复用 fd
4. 发送操作向复用的 fd 写入数据,到达错误客户端
**修复方案**(第二轮修复):
`_close_all_clients()` 改为与 `_remove_client()` 一致的安全模式:先在 `_clients_mtx` 保护下收集所有 (fd, send_lock) 对并清空 map,释放锁后对每个 fd 持有对应的 send_lock 再执行 `shutdown()+close()`:
```cpp
void _close_all_clients() {
std::vector<std::pair<int, std::shared_ptr<std::mutex>>> to_close;
{
std::lock_guard<std::mutex> clk(_clients_mtx);
to_close.reserve(_clients.size());
for (auto& [fd, addr] : _clients) {
auto it = _send_locks.find(fd);
if (it != _send_locks.end()) {
to_close.emplace_back(fd, it->second);
} else {
to_close.emplace_back(fd, nullptr);
}
}
_clients.clear();
_send_locks.clear();
}
for (auto& [fd, send_lock] : to_close) {
if (send_lock) {
std::lock_guard<std::mutex> slk(*send_lock);
::shutdown(fd, SHUT_RDWR);
::close(fd);
} else {
::shutdown(fd, SHUT_RDWR);
::close(fd);
}
}
// ... 清理 _busy/_failed
}
```
---
## 🟠 P1 — 高危问题
### P1-1: 事件循环顺序阻塞接收导致 DoS(Slowloris 变体)✅ 已修复(第一轮)
- **CWE**: CWE-400 (Uncontrolled Resource Consumption)
- **OWASP**: A05:2021
- **位置**: `MsgServer.hpp:424`(原始行号)
- **置信度**: 80%
**问题描述**:
`_event_loop()` 对每个就绪 fd 顺序调用 `_handle_client()`,后者在**事件循环主线程**中阻塞调用 `pmsg_recv(fd, recv_timeout)`(默认 10 秒)。线程池仅处理回调执行,不处理消息接收,即使配置了 `workers > 0` 也无法缓解。攻击者用 `max_connections`(默认 1000)个连接各发送不完整帧头(如 3 字节),可使事件循环阻塞最长 `1000 × 10 = 10000` 秒(约 2.8 小时),期间服务完全无法处理任何新连接或已有消息。
**修复方案**(commit `e92ef05`):
在 `workers > 0` 模式下,`_handle_client()` 不再在事件循环线程调用 `pmsg_recv()`,而是提交 `needs_recv=true` 的任务给 worker 线程,由 worker 线程负责完整的 recv + callback 流程。同步模式(`workers=0`)行为不变。
---
## 🟡 P2 — 中危问题
### P2-1: 错误信息路径脱敏正则不完整,可能泄露内部路径 ✅ 已修复(第一轮)
- **CWE**: CWE-209 (Information Exposure Through an Error Message)
- **OWASP**: A04:2021
- **位置**: `RpcServer.hpp:184`(原始行号)
- **置信度**: 75%
**问题描述**:
`RpcServer::_sanitize_error()` 使用正则 `/[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)*` 替换绝对路径,含空格、括号、`@`、`~` 等特殊字符的路径不被匹配,会原文返回给远程客户端。
**修复方案**(commit `e92ef05`):
正则扩展为 `/\S+(?:/\S*)*`,覆盖所有非空白字符路径。
---
### P2-2: pmsg_recv(int) 依赖隐式SO_RCVTIMEO前置条件,误用可导致永久阻塞 ✅ 已修复(第二轮)
- **CWE**: CWE-400 (Uncontrolled Resource Consumption) / CWE-672 (Operation on a Resource after Expiration)
- **位置**: `Msg.hpp` `pmsg_recv(int sockfd)`
- **置信度**: 70%
**问题描述**:
单参数版本 `pmsg_recv(int sockfd)`(阻塞模式)依赖调用方**预先**通过 `setsockopt(SO_RCVTIMEO)` 设置接收超时,函数本身既不设置超时,也不检查超时是否已设置。如果外部用户直接使用此 API 而忘记设置超时,`recv()` 会永久阻塞,构成拒绝服务风险。虽然所有内部调用点(`MsgClient::listen()`)都正确设置了超时,但作为公共 API 存在误用风险。
**修复方案**(第二轮修复):
1. 在 Doxygen 注释中明确标注前置条件,推荐使用 deadline 模式 `pmsg_recv(sockfd, timeout_sec)`
2. 统一了接收路径的 UTF-8 校验(见 P2-5)
---
### P2-3: 公共API参数无边界校验,异常输入可导致未定义行为 ✅ 已修复(第二轮)
- **CWE**: CWE-20 (Improper Input Validation)
- **位置**: `Msg.hpp`、`MsgServer.hpp`、`AsyncQueue.hpp`、`RpcClient.hpp`、`RpcServer.hpp` 各构造函数和公共方法
- **置信度**: 85%
**问题描述**:
多数公共入口函数未校验参数合法性:
- `timeout_sec` 传入 0 或负数时,`deadline` 立即到期导致异常行为;传入极大值(如 > 1小时)可绕过 Slowloris 防护
- `port` 传入 0 或 >65535 时,`htons()` 截断产生错误端口
- `MsgServerConfig` 中 `workers`、`max_connections`、`max_queue_size`、`poll_timeout`、`recv_timeout`、`listen_backlog` 无范围校验,传入负值或极大值可导致内存分配失败、CPU 100% 或绕过安全限制
- `AsyncQueueConfig` 中 `batch_size`、`flush_interval`、`poll_timeout` 无校验,`batch_size=0` 导致死循环或异常
- `RpcClient` 的 `timeout`、`retry_interval` 无校验
- `RpcServer::add_route()` 接受空字符串路由名和空 handler,导致后续调用崩溃
- `AsyncQueue` 构造接受 null handler,调用时崩溃
**修复方案**(第二轮修复):
新增 `detail::validateTimeout()` 和 `detail::validatePort()` 辅助函数,并在以下入口点添加边界校验:
| 位置 | 校验项 |
|------|--------|
| `detail::recvFrameDeadline()` | `0 < timeout_sec ≤ 300` 秒 |
| `detail::openConnection()` | `1 ≤ port ≤ 65535`,`0 < timeout ≤ 300` 秒 |
| `MsgClient::MsgClient()` | port 范围 |
| `MsgClient::call()` | timeout、retry_interval 范围 |
| `MsgServer::MsgServer()` | port/workers/recv_timeout/max_connections/max_queue_size/listen_backlog/poll_timeout 全部校验 |
| `AsyncQueue::AsyncQueue()` | handler 非空、batch_size>0、flush_interval/poll_timeout 合理范围 |
| `RpcClient::RpcClient()` | timeout、retry_interval 范围 |
| `RpcServer::add_route()` | name 非空、handler 非空 |
---
### P2-4: 消息载荷缺少UTF-8合法性校验 ✅ 已修复(第二轮)
- **CWE**: CWE-20 (Improper Input Validation)
- **位置**: `Msg.hpp` `pmsg_recv()` 两个重载版本
- **置信度**: 65%
**问题描述**:
Python 版 `pymsg.recv()` 在接收载荷后调用 `payload.decode("utf-8")` 进行 UTF-8 合法性校验,非法字节序列会抛出 `UnicodeDecodeError` 并被包装为 `ConnectionError`。C++ 版直接将原始字节存入 `std::string` 返回,不校验 UTF-8 合法性,可能导致上层 JSON 解析器或业务逻辑出现未定义行为(如 null 字节截断、非法序列解析异常、安全绕过)。
**修复方案**(第二轮修复):
1. 新增 `detail::isValidUtf8()` 函数,实现完整的 UTF-8 校验:
- 正确识别 1/2/3/4 字节序列
- 拒绝 overlong 编码(如 0xC0 0x80 表示 NUL)
- 拒绝 UTF-16 代理对(0xD800-0xDFFF)
- 拒绝超 U+10FFFF 的编码
- 拒绝非法续字节、截断序列
2. `pmsg_recv(int)` 和 `pmsg_recv(int, double)` 在接收完 payload 后调用 isValidUtf8 校验,非法载荷抛出 `MsgConnectionException("invalid UTF-8 payload")` 断开连接
---
### P2-5: 缺少参数校验导致无效配置可绕过安全限制 ✅ 已修复(第二轮)
- **CWE**: CWE-20 (Improper Input Validation)
- **说明**: 此问题已合并入 P2-3 修复。MsgServer 构造函数现在对所有配置项实施范围校验:
- `recv_timeout`: (0, 300] 秒,防止传入超大值绕过 Slowloris 防护
- `max_connections`: [1, 100000],防止 0 或负值导致所有连接被拒绝或无限制
- `max_queue_size`: [1, 1000000],防止 0 或负值导致无法提交任务
- `workers`: [0, 256],防止负值或过大线程数耗尽资源
- `poll_timeout`: (0, 5] 秒,防止 0 导致 select 忙轮询 CPU 100%
- `listen_backlog`: [1, 4096]
---
## 🟠 P1 — 高危问题(第三轮)
### P1-2: 服务端 socket 未设置发送超时,慢客户端不读数据可致 send 永久阻塞(DoS) ✅ 已修复(第三轮)
- **CWE**: CWE-400 (Uncontrolled Resource Consumption)
- **OWASP**: A05:2021
- **位置**: `MsgServer.hpp::_accept_client()`(新连接只设 TCP_NODELAY,未设 SO_SNDTIMEO)
- **置信度**: 85%
**问题描述**:
服务端每个客户端 socket 在 `_accept_client()` 中仅设置了 `TCP_NODELAY`,**未设置发送超时 `SO_SNDTIMEO`**。若攻击者连接后停止读取数据,发送缓冲区会被填满,此时 `reply()`/`broadcast()` 调用的 `pmsg_send()` → `sendAll()` 中的 `send()` 因无法写入而**永久阻塞**(无超时则不会返回 EAGAIN)。
- **同步模式(workers=0)**: `reply()`/`broadcast()` 在主线程(事件循环线程)内执行,send 永久阻塞即卡死整个事件循环,服务完全无法处理任何连接 → **服务级 DoS**。
- **线程池模式(workers>0)**: 阻塞的 send 占用 worker 线程,攻击者可用少量慢连接耗尽全部 worker。
**触发场景**:
1. 攻击者建立连接但不调用 recv 读取数据
2. 服务端向其 reply() 大量数据(如日志/广播),发送缓冲区填满
3. `send()` 永久阻塞,卡死主线程(同步模式)或占用 worker 线程
**修复方案**(第三轮修复):
在 `_accept_client()` 中对新连接设置 `SO_SNDTIMEO = 5.0` 秒。发送超时后 `sendAll()` 检测到 `EAGAIN/EWOULDBLOCK` 抛 `MsgTimeoutException`,被 `reply()`/`broadcast()` 捕获并标记为 `_failed`,由主循环统一清理连接(配合 P2-6 修复)。
## 🟡 P2 — 中危问题(第三轮)
### P2-6: reply() 发送失败静默忽略且不标记 failed,连接残留不清理 ✅ 已修复(第三轮)
- **CWE**: CWE-703 (Improper Check or Handling of Exceptional Conditions)
- **位置**: `MsgServer.hpp::reply()`(`catch (const MsgException&)` 分支)
- **置信度**: 80%
**问题描述**:
`broadcast()` 发送失败时会将该 fd 加入 `_failed` 集合,由主循环统一清理;而 `reply()` 发送失败时仅 `catch` 静默忽略,**不标记 `_failed`**。若 reply 的目标连接已断开或发送超时,该连接会一直残留在 `_clients` 中,直到主循环 select 检测到读端异常才被清理,造成资源滞留和清理路径不一致。
**修复方案**(第三轮修复):
`reply()` 的失败分支改为与 `broadcast()` 一致:捕获 `MsgException` 后向 `_failed` 集合插入该 fd,由主循环 `_reap_failed()` 统一清理。
## 🟢 P3 — 优化建议(第三轮)
### P3-1: RpcClient::call() 未校验 success 字段类型,非 bool 时抛未捕获的 boost 异常 ✅ 已修复(第三轮)
- **CWE**: CWE-20 (Improper Input Validation)
- **位置**: `RpcClient.hpp::call()`(`obj.at("success").as_bool()`)
- **置信度**: 70%
**问题描述**:
`call()` 已检查响应包含 `success` 字段,但未校验其**类型**。若远端返回 `{"success": "false"}`(字符串)等非 bool 值,`as_bool()` 会抛出未捕获的 `boost::system::system_error`,而非统一的 `RpcError`,导致调用方无法按既定异常层次处理,且错误信息可能泄露内部细节。
**修复方案**(第三轮修复):
调用 `as_bool()` 前先检查 `success.is_bool()`,类型不符时抛出 `RpcError("非法 RPC 响应(success 字段不是 bool)")`,与其余非法响应处理保持一致。
---
## 🟠 P1 — 高危问题(第四轮)
### P1-3: reply/broadcast 与 _remove_client 之间存在残余竞态,fd 复用可致数据发往错误客户端 ✅ 已修复(第四轮)
- **CWE**: CWE-362 (Race Condition)
- **OWASP**: A01:2021
- **位置**: `MsgServer.hpp` `reply()` / `broadcast()` / `_remove_client()`
- **置信度**: 85%
**问题描述**:
第一轮 P0-1 修复确保了"发送期间不会被 close"(reply/broadcast 先获取 send_lock → _remove_client 等待),但未覆盖反向场景:**_remove_client 先获取 send_lock 并 close(fd) 后,reply/broadcast 后获取 send_lock 时 fd 已关闭**。如果 OS 将该 fd 复用给新 accept 的连接,reply/broadcast 的 `pmsg_send()` 会向复用的 fd 写入数据,导致消息到达错误客户端。
**触发场景**:
1. reply/broadcast 在 `_clients_mtx` 下获取了 `send_lock` 的 shared_ptr 副本
2. 释放 `_clients_mtx`
3. `_remove_client()` 获取 `_clients_mtx`,erase fd,获取 `send_lock` 副本,释放 `_clients_mtx`
4. `_remove_client()` 先于 reply/broadcast 获取 `send_lock`
5. `_remove_client()` `close(fd)`,释放 `send_lock`
6. OS 将 fd 复用给新 accept 的连接
7. reply/broadcast 获取 `send_lock`,调用 `pmsg_send(fd, msg)` → 数据到达新连接
步骤 2-4 的窗口期虽然很短(仅几条指令),但在高并发场景(大量 broadcast + 连接断开同时发生)下可被触发。
**修复方案**(第四轮修复):
引入 `ConnectionLock` 结构,将 `send_lock`(mutex)和 `active`(atomic\<bool\>)组合在一起:
```cpp
struct ConnectionLock {
std::mutex send_mtx;
std::atomic<bool> active{true};
};
```
- `reply()`/`broadcast()` 在获取 `send_mtx` 后检查 `active`:若为 false 则放弃发送(fd 已被关闭)
- `_remove_client()`/`_close_all_clients()` 在获取 `send_mtx` 后先设置 `active=false`,再 `shutdown()`+`close(fd)`
这样无论谁先获取锁:
- 若 reply/broadcast 先获取锁:`active=true`,正常发送,`_remove_client` 等待发送完成后再 close
- 若 `_remove_client` 先获取锁:设置 `active=false`,close(fd),reply/broadcast 后获取锁时检查 `active=false` 直接 return
---
## 🟡 P2 — 中危问题(第四轮)
### P2-7: _server_fd 非原子访问构成数据竞争(UB) ✅ 已修复(第四轮)
- **CWE**: CWE-362 (Race Condition) / CWE-662 (Improper Synchronization)
- **位置**: `MsgServer.hpp` `_server_fd` 成员
- **置信度**: 90%
**问题描述**:
`_server_fd` 是普通 `int`,在主线程(`_event_loop`)读、`stop()` 线程写,构成 C++ 标准定义的数据竞争(Undefined Behavior)。虽然在实际 64 位系统上 int 读写通常是原子的(最坏情况读取旧值导致 accept 失败),但严格来说是 UB,编译器优化可能产生意外行为。
**修复方案**(第四轮修复):
将 `_server_fd` 改为 `std::atomic<int>`,所有读写使用 `.load()` / `.store()` / `.exchange()`。`stop()` 使用 `exchange(-1)` 原子地认领 fd 并关闭。
---
### P2-8: start() 未校验 on_message 回调非空 ✅ 已修复(第四轮)
- **CWE**: CWE-20 (Improper Input Validation)
- **位置**: `MsgServer.hpp` `start()`
- **置信度**: 85%
**问题描述**:
`start(on_message)` 接受 `std::function` 参数但未校验是否为空。若传入空回调(默认构造的 `std::function`),后续 `_on_message(fd, addr, msg)` 会抛出 `std::bad_function_call`,该异常在 worker 线程中被 catch 但在同步模式下会导致连接断开;在 `_dispatch`(RpcServer)中会导致单个请求失败。虽然不会崩溃服务,但行为不符合预期且错误信息不明确。
**修复方案**(第四轮修复):
`start()` 开头添加校验:`if (!on_message) throw MsgException("on_message callback must not be null");`
---
## 🟢 P3 — 优化建议(第四轮)
### P3-2: RpcServer _route 非 string 类型时错误信息不准确 ✅ 已修复(第四轮)
- **CWE**: CWE-209 (Information Exposure Through an Error Message)
- **位置**: `RpcServer.hpp` `_dispatch()`
- **置信度**: 70%
**问题描述**:
`_dispatch()` 中检查 `_route` 字段时,若字段存在但不是 string 类型(如数字、数组),错误信息统一返回 `"missing _route"`,误导客户端以为字段缺失而非类型错误。
**修复方案**(第四轮修复):
区分两种情况:字段不存在返回 `"missing _route"`,类型不符返回 `"_route must be a string"`。
### P3-3: _accept_client 中 setsockopt 在加入 _clients 之后执行 ✅ 已修复(第四轮)
- **位置**: `MsgServer.hpp` `_accept_client()`
- **置信度**: 60%
**问题描述**:
原实现先将 `client_fd` 加入 `_clients` 和 `_send_locks`(持锁),释放锁后再设置 `TCP_NODELAY` 和 `SO_SNDTIMEO`。虽然新连接此时尚未收到消息,不会有 reply/broadcast 发送数据,但最佳实践是先完成所有 socket 配置再暴露给其他线程。
**修复方案**(第四轮修复):
调整顺序:先设置 `TCP_NODELAY` 和 `SO_SNDTIMEO`,再持锁加入 `_clients`。
---
## ✅ 已有安全机制(更新后)
| 机制 | 位置 | 说明 |
|------|------|------|
| 消息大小上限 64 MiB | `Msg_Basic.hpp` `kMaxMsgSize` | 防止超大消息耗尽内存 |
| deadline 模式 recv 超时 | `Msg.hpp` `recvFrameDeadline` | header+payload 共享总超时,防 Slowloris |
| 入口参数边界校验 | `Msg.hpp/MsgServer.hpp/AsyncQueue.hpp/Rpc*.hpp` | timeout/port/配置项全范围校验,拒绝非法输入 |
| UTF-8 载荷校验 | `Msg.hpp` `isValidUtf8()` | 与 Python decode('utf-8') 对齐,拒绝非法字节序列 |
| 最大连接数限制 | `MsgServer.hpp` `max_connections=1000` | 超限拒绝新连接 |
| 有界任务队列 | `MsgServer.hpp` `max_queue_size=1000` | 队列满时断开连接,防 OOM |
| per-connection 发送锁 + active 标志 | `MsgServer.hpp` `ConnectionLock` | reply/broadcast 线程安全防帧交错 + active 检查防 fd 复用后发送 |
| 服务端发送超时 SO_SNDTIMEO | `MsgServer.hpp` `_accept_client()` | 慢客户端不读数据时 send 超时抛异常,防拒绝服务(P1-2) |
| reply() 失败标记 failed | `MsgServer.hpp` `reply()` | 与 broadcast 一致,失败连接由主循环清理(P2-6) |
| RpcClient success 类型校验 | `RpcClient.hpp` `call()` | 非 bool 时抛 RpcError,而非未捕获的 boost 异常(P3-1) |
| _close_all_clients 批量关闭也持锁 | `MsgServer.hpp` `_close_all_clients()` | 修复批量关闭路径的竞态(P0-2) |
| **新增**: ConnectionLock active 标志 | `MsgServer.hpp` `ConnectionLock::active` | reply/broadcast 获取锁后检查 active,彻底消除残余竞态(P1-3) |
| **新增**: _server_fd atomic 访问 | `MsgServer.hpp` `std::atomic<int> _server_fd` | 消除 stop() 与 _event_loop 的数据竞争 UB(P2-7) |
| **新增**: start() 空回调校验 | `MsgServer.hpp` `start()` | 防止 bad_function_call 异常(P2-8) |
| **新增**: _accept_client 先配置后注册 | `MsgServer.hpp` `_accept_client()` | socket 选项在加入 _clients 前设置完整(P3-3) |
| 同一连接消息串行 | `MsgServer.hpp` `_busy` 集合 | 回调执行中暂停该连接 select 监听 |
| JSON 解析深度限制 | `RpcServer.hpp` `opts.max_depth=32` | 防深度嵌套 JSON 导致栈溢出 |
| 异常信息脱敏 | `RpcServer.hpp` `_sanitize_error()` | 过滤绝对路径,完整异常由 on_error 钩子记录 |
| RpcClient 响应格式校验 | `RpcClient.hpp` | 校验 success 字段,非法响应抛 RpcError |
| worker 线程异常隔离 | `MsgServer.hpp` `_worker_loop()` | handler 异常仅断开该连接,不影响服务 |
| select FD_SETSIZE 检查 | `MsgServer.hpp` `_accept_client()` | fd >= FD_SETSIZE 时拒绝连接,防 select 越界 |
| TCP_NODELAY | `MsgServer.hpp` `_accept_client()` | 新连接禁用 Nagle 算法,降低小包延迟 |
| MSG_NOSIGNAL 兼容 | `Msg.hpp` | macOS 兼容,防止 send 到关闭连接触发 SIGPIPE |
| AsyncQueue 背压保护 | `AsyncQueue.hpp` `max_queue_size` | 队列满时丢最旧数据,防内存无限增长 |
| AsyncQueue fail-safe | `AsyncQueue.hpp` | handler 异常触发 on_error,不传播到业务线程 |
| 机制 | 位置 | 说明 |
| -------------------------------- | ----------------------------------------------- | ---------------------------------------------- |
| 消息大小上限 64 MiB | `Msg_Basic.hpp` `kMaxMsgSize` | 防止超大消息耗尽内存 |
| deadline 模式 recv 超时 | `Msg.hpp` `recvFrameDeadline` | header+payload 共享总超时,防 Slowloris |
| 入口参数边界校验 | `Msg.hpp/MsgServer.hpp/AsyncQueue.hpp/Rpc*.hpp` | timeout/port/配置项全范围校验,拒绝非法输入 |
| UTF-8 载荷校验 | `Msg.hpp` `isValidUtf8()` | 与 Python decode('utf-8') 对齐,拒绝非法字节序列 |
| 最大连接数限制 | `MsgServer.hpp` `max_connections=1000` | 超限拒绝新连接 |
| 有界任务队列 | `MsgServer.hpp` `max_queue_size=1000` | 队列满时断开连接,防 OOM |
| per-connection 发送锁 + active 标志 | `MsgServer.hpp` `ConnectionLock` | reply/broadcast 线程安全防帧交错 + active 检查防 fd 复用后发送 |
| 服务端发送超时 SO_SNDTIMEO | `MsgServer.hpp` `_accept_client()` | 慢客户端不读数据时 send 超时抛异常,防拒绝服务(P1-2) |
| reply() 失败标记 failed | `MsgServer.hpp` `reply()` | 与 broadcast 一致,失败连接由主循环清理(P2-6) |
| RpcClient success 类型校验 | `RpcClient.hpp` `call()` | 非 bool 时抛 RpcError,而非未捕获的 boost 异常(P3-1) |
| \_close_all_clients 批量关闭也持锁 | `MsgServer.hpp` `_close_all_clients()` | 修复批量关闭路径的竞态(P0-2) |
| **新增**: ConnectionLock active 标志 | `MsgServer.hpp` `ConnectionLock::active` | reply/broadcast 获取锁后检查 active,彻底消除残余竞态(P1-3) |
| **新增**: \_server_fd atomic 访问 | `MsgServer.hpp` `std::atomic<int> _server_fd` | 消除 stop() 与 \_event_loop 的数据竞争 UB(P2-7) |
| **新增**: start() 空回调校验 | `MsgServer.hpp` `start()` | 防止 bad_function_call 异常(P2-8) |
| **新增**: \_accept_client 先配置后注册 | `MsgServer.hpp` `_accept_client()` | socket 选项在加入 \_clients 前设置完整(P3-3) |
| 同一连接消息串行 | `MsgServer.hpp` `_busy` 集合 | 回调执行中暂停该连接 select 监听 |
| JSON 解析深度限制 | `RpcServer.hpp` `opts.max_depth=32` | 防深度嵌套 JSON 导致栈溢出 |
| 异常信息脱敏 | `RpcServer.hpp` `_sanitize_error()` | 过滤绝对路径,完整异常由 on_error 钩子记录 |
| RpcClient 响应格式校验 | `RpcClient.hpp` | 校验 success 字段,非法响应抛 RpcError |
| worker 线程异常隔离 | `MsgServer.hpp` `_worker_loop()` | handler 异常仅断开该连接,不影响服务 |
| select FD_SETSIZE 检查 | `MsgServer.hpp` `_accept_client()` | fd >= FD_SETSIZE 时拒绝连接,防 select 越界 |
| TCP_NODELAY | `MsgServer.hpp` `_accept_client()` | 新连接禁用 Nagle 算法,降低小包延迟 |
| MSG_NOSIGNAL 兼容 | `Msg.hpp` | macOS 兼容,防止 send 到关闭连接触发 SIGPIPE |
| AsyncQueue 背压保护 | `AsyncQueue.hpp` `max_queue_size` | 队列满时丢最旧数据,防内存无限增长 |
| AsyncQueue fail-safe | `AsyncQueue.hpp` | handler 异常触发 on_error,不传播到业务线程 |
---
## 待修复 / 已知限制(架构层面,非代码缺陷)
| # | 问题 | 优先级 | 说明 |
|---|------|--------|------|
| L-1 | 无认证/鉴权机制 | P0(已缓解) | 仅依赖绑定 127.0.0.1 网络层隔离,任意本地进程可调用所有路由。设计如此,内网微服务信任模型。 |
| L-2 | 无 TLS 加密传输 | P1 | 内网明文 TCP,进程间通信可被本地 root 用户抓包。如跨主机部署需增加 TLS 层。 |
| L-3 | 同步模式(workers=0)仍在主线程阻塞 recv | P2 | 无线程池时无法并发,Slowloris 防护依赖 recv_timeout 截止时间(10秒)。建议生产环境使用 workers>0。 |
| L-4 | select 模型上限 FD_SETSIZE(通常 1024) | P2 | 高并发场景建议改用 epoll/kqueue,或在 fd 超限时告警/拒绝。已在 _accept_client 加检查防止越界。 |
| L-5 | 安全事件无结构化日志 | P3 | 超时、超大消息、UTF-8 校验失败、JSON 解析失败等仅 fprintf(stderr),无可观测性。建议接入结构化日志。 |
| L-6 | pmsg_recv(int) 单参数版本仍依赖外部超时设置 | P3 | 已添加文档警告,推荐使用 deadline 模式。考虑未来版本弃用单参数版本或内部设置安全默认超时。 |
| L-7 | AsyncQueue 关闭时 flush 失败数据只记 on_error | P3 | _flush_all() 中 handler 失败后数据不重试,符合非关键路径设计,但应确保 on_error 有持久化/告警机制。 |
| L-8 | 半连接(只发部分帧头)仍可占用一个 worker 线程 recv_timeout 秒 | P2 | workers>0 时消息接收在 worker 线程完成,慢连接可占用 worker 最多 recv_timeout(默认10秒)。数量受 max_connections 与 recv_timeout 有界约束,已缓解。可用更小 recv_timeout 或独立接收线程池进一步收紧。 |
| L-9 | pmsg_send 发送端不校验 UTF-8,与 Python 版 encode('utf-8') 不对称 | P3 | 接收端已在 pmsg_recv 校验 UTF-8(P2-4),非法载荷会被远端断开。发送端未校验,若业务传入二进制可能被远端拒绝。若存在纯二进制传输场景,发送端校验会破坏兼容,故暂不强制,仅记录。 |
| L-10 | pmsg_send 公共 API 依赖 SO_SNDTIMEO | P3 | sendAll() 在 send() 返回 EAGAIN 时抛超时异常,但前提是 socket 已设置 SO_SNDTIMEO。已添加文档警告,MsgClient 和 MsgServer 内部均已设置。直接使用 pmsg_send 的外部调用方需自行设置。 |
| L-11 | MsgServer 未设置 TCP keepalive | P3 | 客户端崩溃(不发送 FIN)时连接会残留直到 select 检测异常或 send 失败。建议生产环境设置 SO_KEEPALIVE 以更快检测死连接。 |
| L-12 | _remove_client 同步模式下无总超时限制 | P3 | 同步模式下 _handle_client 中 pmsg_recv 有 recv_timeout 限制,但回调执行无超时。慢回调会阻塞事件循环。建议将慢操作移至线程池模式或业务层自行限时。 |
| # | 问题 | 优先级 | 说明 |
| ---- | ----------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| L-1 | 无认证/鉴权机制 | P0(已缓解) | 仅依赖绑定 127.0.0.1 网络层隔离,任意本地进程可调用所有路由。设计如此,内网微服务信任模型。 |
| L-2 | 无 TLS 加密传输 | P1 | 内网明文 TCP,进程间通信可被本地 root 用户抓包。如跨主机部署需增加 TLS 层。 |
| L-3 | 同步模式(workers=0)仍在主线程阻塞 recv | P2 | 无线程池时无法并发,Slowloris 防护依赖 recv_timeout 截止时间(10秒)。建议生产环境使用 workers>0。 |
| L-4 | select 模型上限 FD_SETSIZE(通常 1024) | P2 | 高并发场景建议改用 epoll/kqueue,或在 fd 超限时告警/拒绝。已在 \_accept_client 加检查防止越界。 |
| L-5 | 安全事件无结构化日志 | P3 | 超时、超大消息、UTF-8 校验失败、JSON 解析失败等仅 fprintf(stderr),无可观测性。建议接入结构化日志。 |
| L-6 | pmsg_recv(int) 单参数版本仍依赖外部超时设置 | P3 | 已添加文档警告,推荐使用 deadline 模式。考虑未来版本弃用单参数版本或内部设置安全默认超时。 |
| L-7 | AsyncQueue 关闭时 flush 失败数据只记 on_error | P3 | \_flush_all() 中 handler 失败后数据不重试,符合非关键路径设计,但应确保 on_error 有持久化/告警机制。 |
| L-8 | 半连接(只发部分帧头)仍可占用一个 worker 线程 recv_timeout 秒 | P2 | workers>0 时消息接收在 worker 线程完成,慢连接可占用 worker 最多 recv_timeout(默认10秒)。数量受 max_connections 与 recv_timeout 有界约束,已缓解。可用更小 recv_timeout 或独立接收线程池进一步收紧。 |
| L-9 | pmsg_send 发送端不校验 UTF-8,与 Python 版 encode('utf-8') 不对称 | P3 | 接收端已在 pmsg_recv 校验 UTF-8(P2-4),非法载荷会被远端断开。发送端未校验,若业务传入二进制可能被远端拒绝。若存在纯二进制传输场景,发送端校验会破坏兼容,故暂不强制,仅记录。 |
| L-10 | pmsg_send 公共 API 依赖 SO_SNDTIMEO | P3 | sendAll() 在 send() 返回 EAGAIN 时抛超时异常,但前提是 socket 已设置 SO_SNDTIMEO。已添加文档警告,MsgClient 和 MsgServer 内部均已设置。直接使用 pmsg_send 的外部调用方需自行设置。 |
| L-11 | MsgServer 未设置 TCP keepalive | P3 | 客户端崩溃(不发送 FIN)时连接会残留直到 select 检测异常或 send 失败。建议生产环境设置 SO_KEEPALIVE 以更快检测死连接。 |
| L-12 | \_remove_client 同步模式下无总超时限制 | P3 | 同步模式下 \_handle_client 中 pmsg_recv 有 recv_timeout 限制,但回调执行无超时。慢回调会阻塞事件循环。建议将慢操作移至线程池模式或业务层自行限时。 |
---
@@ -430,32 +124,32 @@ struct ConnectionLock {
**代码变更文件**:
| 文件 | 变更内容 |
|------|----------|
| 文件 | 变更内容 |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [MsgServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/MsgServer.hpp) | **P1修复**: 引入 `ConnectionLock` 结构(mutex + atomic active),`reply()`/`broadcast()` 获取锁后检查 `active`,`_remove_client()`/`_close_all_clients()` close 前设置 `active=false`,彻底消除残余竞态(P1-3);**P2修复**: `_server_fd` 改为 `std::atomic<int>`,所有访问使用 `.load()`/`.store()`/`.exchange()`(P2-7);**P2修复**: `start()` 校验 `on_message` 非空(P2-8);**P3修复**: `_accept_client()` 中 setsockopt 移到加入 `_clients` 之前(P3-3) |
| [RpcServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcServer.hpp) | **P3修复**: `_dispatch()` 区分 `_route` 字段缺失和类型不符的错误信息(P3-2) |
| [Msg.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/Msg.hpp) | **文档**: `pmsg_send` 添加 SO_SNDTIMEO 前置条件警告(L-10) |
| [RpcServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcServer.hpp) | **P3修复**: `_dispatch()` 区分 `_route` 字段缺失和类型不符的错误信息(P3-2) |
| [Msg.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/Msg.hpp) | **文档**: `pmsg_send` 添加 SO_SNDTIMEO 前置条件警告(L-10) |
### 2026-08-07(第三轮审计修复)
**代码变更文件**:
| 文件 | 变更内容 |
|------|----------|
| 文件 | 变更内容 |
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| [MsgServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/MsgServer.hpp) | **P1修复**: `_accept_client()` 对新连接设置 `SO_SNDTIMEO=5s`,慢客户端不读数据时 send 超时抛异常,防服务级 DoS(P1-2);**P2修复**: `reply()` 发送失败标记 `_failed`,由主循环清理连接(P2-6) |
| [RpcClient.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcClient.hpp) | **P3修复**: `call()` 调用 `as_bool()` 前校验 `success` 字段为 bool,类型不符抛 RpcError(P3-1) |
| [RpcClient.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcClient.hpp) | **P3修复**: `call()` 调用 `as_bool()` 前校验 `success` 字段为 bool,类型不符抛 RpcError(P3-1) |
### 2026-08-07(第二轮审计修复)
**代码变更文件**:
| 文件 | 变更内容 |
|------|----------|
| [Msg.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/Msg.hpp) | 新增 isValidUtf8()/validateTimeout()/validatePort();recvFrameDeadline 添加参数校验和UTF-8校验;openConnection 添加 port/timeout 校验;pmsg_recv(int) 添加UTF-8校验和文档警告;MsgClient 构造/call 添加参数校验 |
| [MsgServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/MsgServer.hpp) | **P0修复**: _close_all_clients() 改为持 send_lock 关闭并 shutdown;构造函数添加全量配置校验 |
| [AsyncQueue.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/AsyncQueue.hpp) | 添加 <stdexcept>/<string> 头文件;构造函数校验 handler 非空、batch_size、flush_interval、poll_timeout 范围 |
| [RpcClient.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcClient.hpp) | 构造函数校验 timeout、retry_interval 范围 |
| [RpcServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcServer.hpp) | add_route() 校验 name 非空、handler 非空 |
| 文件 | 变更内容 |
| ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Msg.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/Msg.hpp) | 新增 isValidUtf8()/validateTimeout()/validatePort();recvFrameDeadline 添加参数校验和UTF-8校验;openConnection 添加 port/timeout 校验;pmsg_recv(int) 添加UTF-8校验和文档警告;MsgClient 构造/call 添加参数校验 |
| [MsgServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/MsgServer.hpp) | **P0修复**: \_close_all_clients() 改为持 send_lock 关闭并 shutdown;构造函数添加全量配置校验 |
| [AsyncQueue.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/AsyncQueue.hpp) | 添加 <stdexcept>/<string> 头文件;构造函数校验 handler 非空、batch_size、flush_interval、poll_timeout 范围 |
| [RpcClient.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcClient.hpp) | 构造函数校验 timeout、retry_interval 范围 |
| [RpcServer.hpp](file:///Users/qingsong/Documents/blackbird/libmsg/inc/us/RpcServer.hpp) | add_route() 校验 name 非空、handler 非空 |
---
@@ -466,14 +160,14 @@ struct ConnectionLock {
- **第一轮(2026-08-06)**: 3 个问题(P0 竞态、P1 Slowloris、P2 脱敏正则)已修复。
- **第二轮(2026-08-07)**: 5 个问题(P0 批量关闭竞态回归、4 个 P2 健壮性)已修复。
- **第三轮(2026-08-07)**: 3 个新问题(P1 服务端发送超时缺失、P2 reply 失败未清理、P3 RpcClient success 类型校验)已修复。
- **第四轮(2026-08-07)**: 5 个新问题(P1 reply/broadcast 残余竞态、P2 _server_fd 数据竞争、P2 start 空回调校验、P3 _route 错误信息、P3 setsockopt 顺序)已修复。
- **第四轮(2026-08-07)**: 5 个新问题(P1 reply/broadcast 残余竞态、P2 \_server_fd 数据竞争、P2 start 空回调校验、P3 \_route 错误信息、P3 setsockopt 顺序)已修复。
结合四轮审计结果,libmsg 核心通信层的安全状况良好:
- **内存安全**: 无缓冲区溢出、无 use-after-free(fd 生命周期通过 ConnectionLock + shared_ptr + atomic active 严格管理)
- **DoS 防护**: 64MiB 消息上限、deadline 模式接收超时(防Slowloris)、服务端发送超时 SO_SNDTIMEO(防慢客户端耗尽资源)、max_connections 连接数限制、max_queue_size 队列上限、poll_timeout 防忙轮询、全量参数边界校验
- **竞态安全**: 所有 fd 关闭路径均持有 per-connection send_mtx 并设置 active=false,reply/broadcast 获取锁后检查 active,**彻底消除 CWE-362 竞态(含正向和反向场景)**;_server_fd 使用 atomic 消除数据竞争 UB
- **协议健壮性**: UTF-8 收发端校验与 Python 版对齐、RpcClient 响应字段类型校验统一抛 RpcError、RpcServer _route 类型校验
- **竞态安全**: 所有 fd 关闭路径均持有 per-connection send_mtx 并设置 active=false,reply/broadcast 获取锁后检查 active,**彻底消除 CWE-362 竞态(含正向和反向场景)**;\_server_fd 使用 atomic 消除数据竞争 UB
- **协议健壮性**: UTF-8 收发端校验与 Python 版对齐、RpcClient 响应字段类型校验统一抛 RpcError、RpcServer \_route 类型校验
- **异常安全**: worker 线程异常隔离、AsyncQueue fail-safe、Handler 异常不崩溃服务、RPC 错误脱敏、reply/broadcast 失败连接统一清理、start 空回调校验
遗留的 L-1~L-12 项为架构层面的已知限制/设计决策,非代码缺陷,可根据实际部署需求决定是否增强。
+52 -86
View File
@@ -6,33 +6,44 @@
---
## 代码验证记录
## ⚠️ 合并追溯说明(2026-08-10)
**验证日期**: 2026-08-03
**验证方式**: 逐条对照审计报告与当前代码(`__init__.py`、`rpc.py`、`async_queue.py`)
原始审计报告共 16 条发现(P0×3 / P1×5 / P2×4 / P3×4)。其中 **6 条已修复项**
(P0-1、P0-2、P1-1、P1-2、P1-3、P1-5)的修复代码已落地并经 2026-08-03 验证,
现已作为 `[SECURITY AUDIT ... ✅ 已修复]` 注释合并进源文件对应位置:
### 验证结果汇总
| 编号 | 已合并位置 |
|------|------------|
| P0-1 | `pymsg/__init__.py` — `recv()`、`_recv_exact()` |
| P0-2 | `pymsg/__init__.py` — `recv()`、`_handle_client()` |
| P1-1 | `pymsg/__init__.py` — `MsgServer.__init__`、`_accept_client()` |
| P1-2 | `pymsg/__init__.py` — `_BoundedThreadPoolExecutor` 类、`MsgServer.__init__`、`_handle_client()` |
| P1-3 | `pymsg/rpc.py` — `_sanitize_error()` |
| P1-5 | `pymsg/rpc.py` — 模块顶部 `_MAX_NUM_DIGITS` 注释、`_dispatch()` |
| 等级 | 总数 | 已修复 | 部分修复 | 未修复 | 修复率 |
|------|------|--------|----------|--------|--------|
| P0 | 3 | 2 | 0 | 1 | 67% |
| P1 | 5 | 4 | 0 | 1 | 80% |
| P2 | 4 | 0 | 0 | 4 | 0% |
| P3 | 4 | 0 | 0 | 4 | 0% |
| **合计** | **16** | **6** | **0** | **10** | **37.5%** |
**本报告以下章节仅保留尚未修复(未合并)的 10 条发现作为待办跟踪**,已修复条目
已从「逐条验证」「修复进度」及详细问题章节中删除,其完整描述见源代码注释。
### 逐条验证
---
| 编号 | 状态 | 代码位置 | 验证说明 |
|------|------|----------|----------|
| P0-1 | ✅ 已修复 | `__init__.py` L103-120, L140-170 | `recv()` 改为 deadline 模式:`_recv_exact` 接收 `time.monotonic()+timeout` 作为总截止时刻,每次 `recv_into` 前按剩余时间收紧 socket 超时;header 与 payload 共享同一总超时,逐字节发送无法刷新 deadline,半开连接被及时关闭(已实测:0.6s 超时 0.60s 即抛 TimeoutError) |
| P0-2 | ✅ 已修复 | `__init__.py` L113-114, L462-463 | 双重防护:`recv()` 将 `UnicodeDecodeError` 包装为 `ConnectionError`;`_handle_client` 防御性捕获 `UnicodeDecodeError` |
## 剩余未修复条目(待跟踪)
### 汇总
| 等级 | 剩余总数 | 说明 |
|------|----------|------|
| P0 | 1 | 无认证/鉴权机制 |
| P1 | 1 | 明文 TCP,无 TLS |
| P2 | 4 | 健壮性/配置问题 |
| P3 | 4 | 优化建议 |
| **合计** | **10** | |
### 逐条验证(仅未合并项)
| 编号 | 状态 | 代码位置 | 说明 |
|------|------|----------|------|
| P0-3 | ❌ 未修复 | `rpc.py` L80-171 | 无认证机制,仅依赖 127.0.0.1 网络层隔离 |
| P1-1 | ✅ 已修复 | `__init__.py` L342-350, L467-484 | `MsgServer` 新增 `max_connections` 参数(默认 1000),`_accept_client` 在 `len(_clients) >= max_connections` 时直接拒绝并关闭新连接;已实测 max_connections=3 时第 4 个连接被关闭(ConnectionResetError),handler 只处理前 3 条 |
| P1-2 | ✅ 已修复 | `__init__.py` L316-333, L369, L405-410, L530-540 | 新增 `_BoundedThreadPoolExecutor`(`submit` 时检查 `_work_queue.qsize()`,超限抛 `queue.Full`);`MsgServer` 新增 `max_queue_size` 参数(默认 1000),`_handle_client` 捕获 `queue.Full` 后拒绝并断开该连接;已实测 workers=1 + max_queue_size=1 时,慢 handler 阻塞下第 3 个连接被拒绝关闭,handler 只处理前 2 个任务 |
| P1-3 | ✅ 已修复 | `rpc.py` L49-57, L160-178 | 新增 `_sanitize_error()`:用 `_PATH_RE` 正则将异常信息中的绝对路径(如 `/opt/micro_servers/db/xxx.py`)替换为 `<redacted>`,保留业务错误语义;`_dispatch` 返回给客户端前统一调用;已实测异常信息含绝对路径时返回体仅含 `<redacted>`,不透传 `/opt/...` |
| P1-4 | ❌ 未修复 | 全模块 | 明文 TCP,无 TLS 支持 |
| P1-5 | ✅ 已修复 | `rpc.py` L28-46, L158-166 | `json.loads` 增加 `parse_int=_checked_int` / `parse_float=_checked_float` 钩子,数字位数超 `_MAX_NUM_DIGITS=100` 抛 `ValueError`;`except` 捕获 `RecursionError` 防止深度嵌套导致栈溢出;解析失败统一返回 `"invalid JSON request"` 不透传输入片段;已实测超大整数(10000 位)与深度 200000 嵌套均被拒绝,服务存活 |
| P2-1 | ❌ 未修复 | `__init__.py` L106-111 | length=0 空消息无特殊处理 |
| P2-2 | ❌ 未修复 | `__init__.py` L395-418 | `broadcast()` 无频率/权限控制 |
| P2-3 | ❌ 未修复 | `__init__.py` L377-378 | `except (ValueError, OSError): break` 仍捕获所有 OSError(含 EINTR) |
@@ -55,44 +66,20 @@
---
## 修复进度
## 修复进度(剩余)
| 优先级 | 总数 | 已修复 | 待修复 |
|--------|------|--------|--------|
| P0 | 3 | 2 | 1 |
| P1 | 5 | 2 | 3 |
| P2 | 4 | 0 | 4 |
| P3 | 4 | 0 | 4 |
| 优先级 | 剩余待修复 |
|--------|------------|
| P0 | 1(无认证) |
| P1 | 1(无 TLS) |
| P2 | 4 |
| P3 | 4 |
---
## 🔴 P0 — 严重问题
### P0-1: recv 超时可被绕过(Slowloris 变种 DoS)✅ 已修复
- **位置**: `__init__.py:103-120`(`recv`)、`__init__.py:140-170`(`_recv_exact`)
- **原问题**: `sock.settimeout()` 设置的是**单次系统调用**超时,不是整个消息接收总超时。攻击者可每次在快超时时发送 1 字节:
- 4 字节长度头最长可占用 `4 * recv_timeout = 40秒`
- 载荷部分按字节发送可使连接存活任意长时间
- **影响**: 少量攻击者即可耗尽连接/线程资源,造成所有依赖 pymsg 的服务拒绝服务
- **修复方案(deadline 模式)**:
- `recv()` 在开头计算 `deadline = time.monotonic() + timeout`,header 与 payload 共享**同一总截止时刻**
- `_recv_exact` 每次 `recv_into` 前按 `deadline - now` 设置剩余 socket 超时,剩余时间耗尽即抛 `TimeoutError`
- 逐字节发送会不断刷新单次 `sock.settimeout()`,但**无法刷新总 deadline**,半开连接会被及时关闭
- 实测验证:超时 0.6s 时,逐字节攻击在 0.60s 即被 TimeoutError 终止(旧实现需等到完整消息 1.4s)
---
### P0-2: UnicodeDecodeError 可导致同步模式服务崩溃 ✅ 已修复
- **位置**: `__init__.py:113-114`、`__init__.py:462-463`
- **原问题**: `payload.decode("utf-8")` 抛出的 `UnicodeDecodeError`(`ValueError` 子类)不在 `_handle_client` 的异常捕获列表中,同步模式下收到无效 UTF-8 字节会直接导致整个服务进程崩溃
- **修复方案(双重防护)**:
1. `recv()` 函数将 `UnicodeDecodeError` 包装为 `ConnectionError`(第一层)
2. `_handle_client` 异常捕获列表添加 `UnicodeDecodeError`(第二层,防御性兜底)
- **影响范围**: 已消除,db/confs/smtp/captcha 等所有 pymsg 服务均受保护
---
### P0-3: 无认证/鉴权机制(已部分缓解:绑定 127.0.0.1)
### P0-3: 无认证/鉴权机制
- **位置**: `rpc.py:80-171` 整个 RpcServer
- **问题**: RPC 服务完全没有身份验证,任何能连接到端口的客户端都能调用所有路由
- **当前缓解**: 所有服务均绑定 127.0.0.1,依赖网络层隔离
@@ -107,11 +94,7 @@
| # | 问题 | 位置 | 状态 |
|---|------|------|------|
| P1-1 | 无连接数限制,连接耗尽攻击 | `__init__.py:342-350,467-484` | ✅ 已修复(`max_connections` 默认 1000) |
| P1-2 | 线程池无队列限制,慢 handler 耗尽内存 | `__init__.py:316-333,369,405-410,530-540` | ✅ 已修复(`_BoundedThreadPoolExecutor` + `max_queue_size` 默认 1000) |
| P1-3 | 异常信息直接返回客户端,泄露内部细节 | `rpc.py:49-57,160-178` 新增 `_sanitize_error()` 脱敏绝对路径 | ✅ 已修复 |
| P1-4 | 无 TLS/加密传输,内网流量可被窃听 | 全模块明文 TCP | ❌ 未修复 |
| P1-5 | JSON 解析无深度限制,深度嵌套导致栈溢出/CPU 100% | `rpc.py:28-46,158-166` `parse_int/parse_float` 钩子 + 捕获 `RecursionError` | ✅ 已修复 |
---
@@ -141,39 +124,22 @@
1. ✅ 64 MiB 消息大小上限(`_MAX_MSG_SIZE`),防止 OOM
2. ✅ send/recv 双端都检查消息大小
3. ✅ recv_timeout 防 Slowloris(基础实现,需修复 P0-1)
4. ✅ 线程池模式下同一连接串行处理,避免帧交错
5. ✅ 使用 `sendmsg` 散射-聚集 I/O 减少内存拷贝
6. ✅ 使用 `recv_into` 预分配 buffer
7. ✅ 线程安全的发送锁
8. ✅ 回调异常隔离(线程池模式)
9. ✅ 单元测试覆盖消息大小边界
3. ✅ 线程池模式下同一连接串行处理,避免帧交错
4. ✅ 使用 `sendmsg` 散射-聚集 I/O 减少内存拷贝
5. ✅ 使用 `recv_into` 预分配 buffer
6. ✅ 线程安全的发送锁
7. ✅ 回调异常隔离(线程池模式)
8. ✅ 单元测试覆盖消息大小边界
## 新增安全机制(审计后添加,审计报告未覆盖)
| 机制 | 代码位置 | 说明 |
|------|----------|------|
| UnicodeDecodeError 双重防护 | `__init__.py` L113-114, L462-463 | `recv()` 包装为 `ConnectionError` + `_handle_client` 防御性捕获,防止无效 UTF-8 导致服务崩溃 |
| AsyncRpcQueue 有界队列 | `async_queue.py` L47, L130-132 | `max_queue_size` 默认 10000,超限丢弃最旧数据,防止内存无限增长 |
| AsyncRpcQueue 错误隔离 | `async_queue.py` L49, L189-194 | `on_error` 回调 + 异常捕获,后台线程失败不影响主业务 |
| RpcClient 响应格式校验 | `rpc.py` L89-90 | 校验响应必须是含 `success` 字段的 dict,非法响应抛出 `RpcError` |
| MsgClient.call 节制日志 | `__init__.py` L209, L248-254 | 重试日志首次立即打印,后续每 5 秒一次,避免刷屏 |
> 注:P0-1(recv 总超时)、P0-2(UnicodeDecodeError 防护)、P1-1(连接数限制)、
> P1-2(有界队列)、P1-3(异常脱敏)、P1-5(JSON 解析限制)等已修复机制均已合并进
> 源代码注释,见开头「合并追溯说明」。
---
## 修复路线图
### 立即(24小时)
- [x] 修复 P0-1: recv 总超时(deadline 模式)✅ 已完成
- [x] 修复 P0-2: 捕获 UnicodeDecodeError/JSONDecodeError ✅ 已完成
### 短期(72小时)
- [x] 添加最大连接数限制 ✅ 已完成
- [x] 线程池改用有界队列 ✅ 已完成
- [x] 异常信息脱敏 ✅ 已完成
- [x] JSON 解析深度限制 ✅ 已完成
## 修复路线图(剩余)
### 中期
- [ ] 统一认证机制
- [ ] TLS 支持选项
- [ ] 安全事件日志
- [ ] 统一认证机制(P0-3)
- [ ] TLS 支持选项(P1-4)
- [ ] 安全事件日志(P3-1)
+2
View File
@@ -90,6 +90,8 @@ public:
_stop(false),
_wakeup(false),
_dropped(0) {
// [libmsg-audit P2-3] 入口校验:handler 非空、batch_size>0、flush_interval∈(0,300]、
// poll_timeout∈(0,5]。拒绝非法输入(如 batch_size=0 导致死循环、poll_timeout=0 忙轮询)。
if (!_handler) {
throw std::invalid_argument("AsyncQueue: handler must not be null");
}
+11 -7
View File
@@ -51,9 +51,9 @@ namespace detail {
using namespace msg_constants;
// ---- UTF-8 合法性校验(对齐 Python decode('utf-8'))------------------------
// ---- UTF-8 合法性校验(对齐 Python decode('utf-8'))------------------------ [libmsg-audit P2-4]
// 防御性校验:协议声明载荷为 UTF-8,非法字节序列表示协议错误或攻击,
// 直接抛 MsgConnectionException 断开连接。
// 直接抛 MsgConnectionException 断开连接(拒绝 overlong / 代理对 / 超 U+10FFFF)。
inline bool isValidUtf8(const char* data, size_t len) noexcept {
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data);
size_t i = 0;
@@ -91,7 +91,9 @@ inline bool isValidUtf8(const char* data, size_t len) noexcept {
return true;
}
// ---- 参数校验辅助 --------------------------------------------------------
// ---- 参数校验辅助 -------------------------------------------------------- [libmsg-audit P2-3]
// validateTimeout / validatePort:统一拒绝非法 timeout((0,300]s)与 port([1,65535]),
// 防止 0/负值导致 deadline 立即到期、极大值绕过 Slowloris 防护、port 越界 htons 截断。
inline void validateTimeout(double timeout_sec) {
if (timeout_sec <= 0 || timeout_sec > 300.0) { // 最大 5 分钟
throw MsgException("invalid timeout: " + std::to_string(timeout_sec) + " seconds (must be in (0, 300])");
@@ -153,9 +155,10 @@ inline void recvExact(int sockfd, char *buf, size_t n) {
}
}
// ---- deadline 模式精确读取 n 字节(总超时,防 Slowloris)-------------------
// ---- deadline 模式精确读取 n 字节(总超时,防 Slowloris)------------------- [libmsg-audit P1-1]
// 与 Python 版 _recv_exact 对齐:每次 select 前按剩余时间收紧超时,
// header+payload 共享同一个 deadline,总接收时间不超过 timeout_sec。
// header+payload 共享同一个 deadline,总接收时间不超过 timeout_sec,
// 防止单连接以不完整帧头长期占用接收线程(Slowloris 变体,CWE-400)。
inline void recvExactDeadline(int sockfd, char *buf, size_t n,
const std::chrono::steady_clock::time_point& deadline) {
@@ -217,7 +220,7 @@ inline std::string recvFrameDeadline(int sockfd, double timeout_sec) {
std::string payload(static_cast<size_t>(length), '\0');
if (length > 0) {
recvExactDeadline(sockfd, payload.data(), length, deadline);
// UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
// [libmsg-audit P2-4] UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
if (!isValidUtf8(payload.data(), length)) {
throw MsgConnectionException("invalid UTF-8 payload");
}
@@ -246,6 +249,7 @@ inline void sendAll(int sockfd, const char *data, size_t len) {
// ---- 短连接 helper:connect → action → close ------------------------------
inline int openConnection(const std::string& addr, int port, double timeout) {
// [libmsg-audit P2-3] 入口边界校验:port 与 timeout 必须在合法范围。
validatePort(port);
validateTimeout(timeout);
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
@@ -317,7 +321,7 @@ inline std::string pmsg_recv(int sockfd) {
std::string payload(static_cast<size_t>(length), '\0');
if (length > 0) {
detail::recvExact(sockfd, payload.data(), length);
// UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
// [libmsg-audit P2-4] UTF-8 合法性校验(对齐 Python decode('utf-8')),非法载荷视为协议错误
if (!detail::isValidUtf8(payload.data(), length)) {
throw MsgConnectionException("invalid UTF-8 payload");
}
+34 -20
View File
@@ -81,9 +81,14 @@ struct MsgServerConfig {
/// 每连接的发送锁和活跃标志。
///
/// active 标志用于防止 CWE-362 竞态:当 _remove_client() 先获取 send_mtx
/// 并 close(fd) 后,reply/broadcast 后获取 send_mtx 时通过 active==false
/// 得知 fd 已关闭,立即放弃发送,避免向 OS 复用的 fd 发送数据。
/// [libmsg-audit P0-1 / P0-2 / P1-3] 组合 send_mtx(互斥锁)+ active(原子标志)彻底消除
/// CWE-362 竞态:
/// - 第一轮 P0-1:单客户端移除路径先持 send_mtx 再 close,reply/broadcast 先获取
/// send_mtx 则 _remove_client 等待,防止 fd 关闭后被 OS 复用导致数据发往错误客户端。
/// - 第二轮 P0-2:批量关闭路径 _close_all_clients 同样持 send_mtx 后 shutdown+close,
/// 修复第一轮只覆盖单路径的竞态回归。
/// - 第四轮 P1-3:active 标志堵住反向竞态——_remove_client 先获取锁并 close(fd) 后,
/// reply/broadcast 后获取锁时通过 active==false 得知 fd 已关闭,立即放弃发送。
struct ConnectionLock {
std::mutex send_mtx;
std::atomic<bool> active{true};
@@ -101,6 +106,10 @@ public:
explicit MsgServer(const std::string& addr, int port, MsgServerConfig config = {})
: _addr(addr), _port(port), _config(config), _server_fd(-1), _running(false) {
// [libmsg-audit P2-3 / P2-5] 全量边界校验:拒绝非法 port / workers / recv_timeout /
// max_connections / max_queue_size / listen_backlog / poll_timeout。防止 htons 截断产生
// 错误端口、负值导致所有连接被拒或队列无法提交、极大值(>300s)绕过 Slowloris 防护、
// poll_timeout=0 导致 select 忙轮询 CPU 100%。
detail::validatePort(port);
if (config.workers < 0 || config.workers > 256) {
throw MsgException("invalid workers: " + std::to_string(config.workers) + " (must be in [0, 256])");
@@ -136,6 +145,8 @@ public:
* 回调内可安全调用 reply()/broadcast()/stop()。
*/
void start(MessageCallback on_message) {
// [libmsg-audit P2-8] 空回调会在 worker 线程 / RpcServer 分发中抛 std::bad_function_call,
// 提前校验并给出明确错误,避免行为不符合预期且错误信息含糊。
if (!on_message) {
throw MsgException("MsgServer::start(): on_message callback must not be null");
}
@@ -233,14 +244,14 @@ public:
conn_lock = it->second;
}
std::lock_guard<std::mutex> lock(conn_lock->send_mtx);
// 检查 active:防止 _remove_client 先获取 send_mtx 并 close(fd) 后,
// reply 向已关闭/被 OS 复用的 fd 发送数据(CWE-362)
// [libmsg-audit P1-3] 检查 active:防止 _remove_client 先获取 send_mtx 并 close(fd) 后,
// reply 向已关闭/被 OS 复用的 fd 发送数据(CWE-362 反向竞态)。
if (!conn_lock->active.load()) return;
try {
pmsg_send(sockfd, msg);
} catch (const MsgException&) {
// 发送失败(连接断开或发送超时),标记为 failed 由主循环清理。
// 与 broadcast() 保持一致,避免连接残留。
// [libmsg-audit P2-6] 发送失败(连接断开或发送超时),标记为 failed 由主循环清理。
// 与 broadcast() 保持一致,避免连接残留不清理(原 reply 静默忽略,连接滞留)。
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(sockfd);
}
@@ -267,12 +278,12 @@ public:
}
for (auto& [fd, conn_lock] : targets) {
std::lock_guard<std::mutex> slock(conn_lock->send_mtx);
// 检查 active:防止 _remove_client 先 close(fd) 后向复用 fd 发送(CWE-362)
// [libmsg-audit P1-3] 检查 active:防止 _remove_client 先 close(fd) 后向复用 fd 发送(CWE-362 反向竞态)
if (!conn_lock->active.load()) continue;
try {
pmsg_send(fd, msg);
} catch (const MsgException&) {
// 发送失败,标记为 failed 由主循环清理
// [libmsg-audit P2-6] 发送失败,标记为 failed 由主循环统一清理
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(fd);
}
@@ -314,7 +325,8 @@ private:
}
if (!has_task) continue;
// workers > 0 模式下,消息接收在 worker 线程中完成(防事件循环阻塞 CWE-400)
// [libmsg-audit P1-1] workers > 0 模式下,消息接收在 worker 线程中完成(防事件循环阻塞 CWE-400),
// 避免攻击者以 max_connections 个半开连接阻塞事件循环长达数小时(Slowloris 变体)。
if (task.needs_recv) {
try {
task.message = pmsg_recv(task.fd, _config.recv_timeout);
@@ -452,12 +464,13 @@ private:
}
if (client_fd >= FD_SETSIZE) { ::close(client_fd); return; }
// 禁用 Nagle 算法和设置发送超时(在加入 _clients 之前,确保连接配置完整)
// [libmsg-audit P3-3] 禁用 Nagle 算法和设置发送超时(在加入 _clients 之前,确保连接配置完整),
// 先完成所有 socket 配置再暴露给其他线程,避免半配置连接被回复。
int flag = 1;
::setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
// 设置发送超时:防止慢客户端不读数据导致发送缓冲区填满,
// 使 reply()/broadcast() 的 send() 永久阻塞(拒绝服务)。
// [libmsg-audit P1-2] 设置发送超时:防止慢客户端不读数据导致发送缓冲区填满,
// 使 reply()/broadcast() 的 send() 永久阻塞(拒绝服务,CWE-400)。
// 超时后 sendAll() 抛 MsgTimeoutException,被 reply/broadcast 捕获并标记 failed 清理。
struct timeval sndtv {};
constexpr double kServerSendTimeout = 5.0; // 秒
@@ -511,7 +524,7 @@ private:
return;
}
// 线程池模式:标记 busy,提交任务(由 worker 负责 recv + callback,
// [libmsg-audit P1-1] 线程池模式:标记 busy,提交任务(由 worker 负责 recv + callback,
// 事件循环线程不阻塞接收,防 Slowloris DoS CWE-400)
{
std::lock_guard<std::mutex> blk(_busy_mtx);
@@ -557,9 +570,9 @@ private:
_send_locks.erase(it);
}
}
// 持有 send_mtx 后设置 active=false 再 close(fd):
// 1. 防止 broadcast/reply 在 fd 关闭后向 OS 复用的新连接发送数据(CWE-362)
// 2. active=false 让 reply/broadcast 在获取锁后得知 fd 已关闭,放弃发送
// [libmsg-audit P0-1 / P1-3] 持有 send_mtx 后设置 active=false 再 close(fd):
// 1. 防止 broadcast/reply 在 fd 关闭后向 OS 复用的新连接发送数据(CWE-362,第一轮 P0-1)
// 2. active=false 让 reply/broadcast 在获取锁后得知 fd 已关闭,放弃发送(第四轮 P1-3 反向竞态)
if (conn_lock) {
std::lock_guard<std::mutex> slk(conn_lock->send_mtx);
conn_lock->active.store(false);
@@ -587,8 +600,9 @@ private:
_clients.clear();
_send_locks.clear();
}
// 持有每个连接的 send_mtx 后设置 active=false 再 close(fd):
// 防止 stop()/析构时 reply/broadcast 正在发送,导致数据发到 OS 复用的新连接(CWE-362)
// [libmsg-audit P0-2 / P1-3] 持有每个连接的 send_mtx 后设置 active=false 再 close(fd):
// 防止 stop()/析构时 reply/broadcast 正在发送,导致数据发到 OS 复用的新连接(CWE-362)。
// 第二轮 P0-2 修复批量关闭路径的竞态回归,第四轮 P1-3 的 active 标志堵住反向竞态。
for (auto& [fd, conn_lock] : to_close) {
if (conn_lock) {
std::lock_guard<std::mutex> slk(conn_lock->send_mtx);
@@ -611,7 +625,7 @@ private:
std::string _addr;
int _port;
MsgServerConfig _config;
std::atomic<int> _server_fd; ///< 监听 socket fd(atomic 防止 stop() 与 _event_loop 数据竞争)
std::atomic<int> _server_fd; ///< [libmsg-audit P2-7] 监听 socket fd(atomic 防止 stop() 与 _event_loop 数据竞争 UB,CWE-362/662)
std::atomic<bool> _running;
MessageCallback _on_message;
+3 -1
View File
@@ -75,6 +75,7 @@ public:
_timeout(timeout),
_retry_on_refused(retry_on_refused),
_retry_interval(retry_interval) {
// [libmsg-audit P2-3] 入口边界校验:timeout ∈ (0,300]s,retry_interval ∈ (0,60]s。
if (timeout <= 0 || timeout > 300.0) {
throw MsgException("invalid timeout: " + std::to_string(timeout));
}
@@ -128,7 +129,8 @@ public:
}
const json::value& success = obj.at("success");
if (!success.is_bool()) {
// 类型不符时不要调用 as_bool()(会抛未捕获的 boost system_error),统一抛 RpcError
// [libmsg-audit P3-1] 类型不符时不要调用 as_bool()(会抛未捕获的 boost system_error),
// 统一抛 RpcError,与其余非法响应处理保持一致,避免底层异常泄露内部细节。
throw RpcError("非法 RPC 响应(success 字段不是 bool)");
}
if (!success.as_bool()) {
+4 -2
View File
@@ -83,6 +83,7 @@ public:
* @param name 路由名(对应请求中的 _route 字段),不能为空
* @param handler 处理函数,签名 json::value(const json::object& req)
*/
// [libmsg-audit P2-3] 入口校验:name 与 handler 均非空,否则后续调用会崩溃。
void add_route(const std::string& name, Handler handler) {
if (name.empty()) {
throw MsgException("route name cannot be empty");
@@ -127,7 +128,7 @@ private:
json::object req = req_val.as_object();
// 提取 _route
// [libmsg-audit P3-2] 提取 _route:区分"字段缺失"与"类型不符"返回不同错误信息,避免误导客户端。
std::string route;
if (auto it = req.find("_route"); it != req.end() && it->value().is_string()) {
route = it->value().as_string().c_str();
@@ -181,9 +182,10 @@ private:
_server.reply(fd, json::serialize(resp));
}
// ---- 错误信息脱敏 ------------------------------------------------------
// ---- 错误信息脱敏 ------------------------------------------------------ [libmsg-audit P2-1]
// 匹配绝对路径(如 /opt/micro_servers/db/xxx.py 或 /usr/lib/...),替换为 <redacted>
// 不返回函数堆栈、SQL、文件路径等内部细节给客户端;完整异常由 on_error 钩子记录到服务端日志。
// 正则已扩展为 /\S+(?:\/\S*)*,覆盖含空格/括号/@/~ 等特殊字符的路径(原正则会泄露此类路径,CWE-209)。
static std::string _sanitize_error(const std::string& msg) {
if (msg.empty()) return msg;
+33 -5
View File
@@ -101,6 +101,12 @@ def recv(
message is received.
TimeoutError: If the operation exceeds the timeout.
"""
# [SECURITY AUDIT P0-1 ✅ 已修复] recv 总超时(deadline 模式)
# 原问题:sock.settimeout() 是「单次系统调用」超时而非整条消息总超时,
# 攻击者可每次在快超时时发 1 字节绕过,造成 Slowloris 型连接耗尽 DoS。
# 修复:header 与 payload 共享同一总截止时刻 deadline,每次 recv_into
# 前按剩余时间收紧 socket 超时,半开连接被及时关闭(实测 0.6s 超时即终止)。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
deadline = None
if timeout is not None:
deadline = time.monotonic() + timeout
@@ -113,6 +119,11 @@ def recv(
)
payload = _recv_exact(sock, length, deadline)
return payload.decode("utf-8")
# [SECURITY AUDIT P0-2 ✅ 已修复] UnicodeDecodeError 导致同步服务崩溃
# 原问题:payload.decode("utf-8") 抛出的 UnicodeDecodeError(ValueError 子类)
# 不在 _handle_client 捕获列表中,同步模式收到无效 UTF-8 会令整个进程崩溃。
# 修复(双重防护):此处将 UnicodeDecodeError 包装为 ConnectionError(第一层),
# _handle_client 再防御性捕获兜底(第二层)。db/confs/smtp/captcha 等服务均受保护。
except UnicodeDecodeError:
raise ConnectionError("invalid UTF-8 payload") from None
except (TimeoutError, _socket.timeout):
@@ -155,6 +166,9 @@ def _recv_exact(
``TimeoutError``。这防止 Slowloris 型攻击:逐字节发送会不断刷新单次
``sock.settimeout()``,但无法刷新总 deadline,半开连接会被及时关闭。
"""
# [SECURITY AUDIT P0-1 ✅ 已修复] 本函数为 P0-1 的核心修复实现,
# 与上方 recv() 的 deadline 计算配合,构成整条消息的总超时防护。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
buf = bytearray(n)
view = memoryview(buf)
got = 0
@@ -313,6 +327,11 @@ class MsgClient:
self._sock = None
# [SECURITY AUDIT P1-2 ✅ 已修复] 线程池无队列限制 → 慢 handler 耗尽内存
# 原问题:标准 ThreadPoolExecutor 使用无界队列,慢 handler 时任务无限堆积直至 OOM。
# 修复:本类在 submit 时检查排队深度,超限即抛 queue.Full;MsgServer 新增
# max_queue_size 参数(默认 1000),_handle_client 捕获 queue.Full 后拒绝并断开连接。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
class _BoundedThreadPoolExecutor(ThreadPoolExecutor):
"""有界线程池:提交队列有上限,满时抛 ``queue.Full``。
@@ -368,7 +387,13 @@ class MsgServer:
def __init__(self, addr: str, port: int, *, workers: int = 0,
recv_timeout: float = _RECV_TIMEOUT,
# [SECURITY AUDIT P1-1 ✅ 已修复] 无连接数限制 → 连接耗尽攻击
# 原问题:服务端无并发连接上限,攻击者可耗尽文件描述符/线程资源。
# 修复:新增 max_connections(默认 1000),_accept_client 在
# len(_clients) >= max_connections 时直接拒绝并关闭新连接。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
max_connections: int = 1000,
# [SECURITY AUDIT P1-2 ✅ 已修复] 见上方 _BoundedThreadPoolExecutor
max_queue_size: int = 1000) -> None:
self.addr = addr
self.port = port
@@ -499,8 +524,9 @@ class MsgServer:
client_sock, client_addr = servsock.accept()
if client_sock.fileno() == -1:
return
# 活跃连接数上限控制:超过 max_connections 直接拒绝,
# 防止连接耗尽攻击(listen backlog 只限制 SYN 队列,不限制已建立连接数)
# [SECURITY AUDIT P1-1 ✅ 已修复] 活跃连接数上限控制:超过
# max_connections 直接拒绝,防止连接耗尽攻击(listen backlog 只限制
# SYN 队列,不限制已建立连接数)。本段即 P1-1 的落地实现。
if len(self._clients) >= self._max_connections:
try:
client_sock.close()
@@ -518,8 +544,9 @@ class MsgServer:
text = recv(s, timeout=self._recv_timeout)
except (ConnectionError, ConnectionResetError, OSError,
TimeoutError, UnicodeDecodeError):
# TimeoutError: 客户端在超时时间内未发送完整消息(Slowloris 防护),关闭连接
# UnicodeDecodeError: 防御性捕获,recv() 已包装为 ConnectionError,此处兜底
# [SECURITY AUDIT P0-2 ✅ 已修复(第二层兜底)] UnicodeDecodeError:
# recv() 已将其包装为 ConnectionError,此处防御性捕获防止任何遗漏。
# TimeoutError:客户端在超时时间内未发送完整消息(Slowloris 防护,P0-1),关闭连接
self._remove_client(s, socketlist)
return
addr = self._clients.get(s, ("unknown", 0))
@@ -537,7 +564,8 @@ class MsgServer:
try:
self._pool.submit(self._run_task, s, addr, text, on_message)
except _queue.Full:
# 有界队列已满:慢 handler 时任务不应无限堆积,拒绝并断开该连接
# [SECURITY AUDIT P1-2 ✅ 已修复] 有界队列已满:慢 handler 时任务不应
# 无限堆积,拒绝并断开该连接,防止内存无限增长。
with self._lock:
self._busy.discard(s)
self._remove_client(s, socketlist)
+14 -1
View File
@@ -25,6 +25,13 @@ from . import MsgClient, MsgServer
__all__ = ["RpcClient", "RpcServer", "RpcError"]
# [SECURITY AUDIT P1-5 ✅ 已修复] JSON 解析无深度/数字位数限制 → 栈溢出 / CPU 100%
# 原问题:json.loads 对深度嵌套与超大数字无限制,恶意输入可触发 RecursionError
# 栈溢出或消耗大量 CPU/内存。修复:parse_int/parse_float 钩子限制数字位数
# <= _MAX_NUM_DIGITS(100),并捕获 RecursionError 防止深度嵌套;解析失败统一返回
# "invalid JSON request" 不透传输入片段(见 _dispatch)。实测 10000 位整数与
# 深度 200000 嵌套均被拒绝,服务存活。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
# 超大数字位数上限:防止超大整数/浮点(如 10^100000)消耗大量 CPU 与内存
_MAX_NUM_DIGITS = 100
@@ -46,6 +53,11 @@ def _checked_float(value: str) -> float:
return float(value)
# [SECURITY AUDIT P1-3 ✅ 已修复] 异常信息直接返回客户端 → 泄露内部细节
# 原问题:handler 抛出的异常 str(e) 含绝对路径(如 /opt/micro_servers/db/xxx.py)、
# 堆栈等内部细节,直接回显给客户端。修复:_dispatch 返回前统一经 _sanitize_error()
# 将绝对路径替换为 <redacted>,保留业务错误语义(实测含绝对路径时仅返回 <redacted>)。
# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md
def _sanitize_error(msg: str) -> str:
"""脱敏异常信息中的绝对路径等内部细节,保留业务错误语义。
@@ -187,7 +199,8 @@ class RpcServer:
# ── 分发与响应包装 --------------------------------------------------
def _dispatch(self, sock, addr, text):
try:
# parse_int/parse_float 限制超大数字;RecursionError 防护深度嵌套
# [SECURITY AUDIT P1-5 ✅ 已修复] parse_int/parse_float 限制超大数字;
# RecursionError 防护深度嵌套(落地实现见文件顶部 _MAX_NUM_DIGITS 注释)
req = json.loads(text, parse_int=_checked_int,
parse_float=_checked_float)
except (json.JSONDecodeError, ValueError, RecursionError):