diff --git a/docs/reports/aibox_open_dev_plan.md b/docs/reports/aibox_open_dev_plan.md new file mode 100644 index 0000000..f84b6e7 --- /dev/null +++ b/docs/reports/aibox_open_dev_plan.md @@ -0,0 +1,194 @@ +# aibox 开放版开发计划 + +## 背景 + +当前 aibox 主分支完整功能包含**命令执行工具**(`run_command`),该功能存在 **P0-1 严重命令注入漏洞**([aibox_SECURITY_AUDIT_20260801.md](../security/aibox_SECURITY_AUDIT_20260801.md)): + +- 使用 `shell=True` + 用户输入拼接,可执行任意系统命令 +- P0-2 WebSocket 无认证,结合可导致远程 RCE +- 不适合在公开网络环境下开放使用 + +**目标**:创建一个**阉割版 aibox**,移除命令执行、文件操作等危险功能,保留核心对话、技能系统、记忆管理、任务调度,可安全开放使用。 + +## 版本分支 + +- `main`:保持现有完整功能,供本地可信环境使用 +- `open`:开放阉割版,移除所有危险功能,安全可公开访问 + +## 功能裁剪方案 + +| 功能 | 主分支完整版 | 开放阉割版 | 备注 | +|------|-------------|------------|------| +| AI 对话(流式输出) | ✅ | ✅ | 保留核心能力 | +| WebSocket 前端交互 | ✅ | ✅ | 需要认证 | +| DeepSeek API 调用 | ✅ | ✅ | 保留 | +| 思考模式(Reasoning) | ✅ | ✅ | 保留 | +| 子代理 | ✅ | ✅ | 保留(无工具调用) | +| 情绪表达(情绪化技能) | ✅ | ✅ | 保留 | +| 记忆搜索/保存 | ✅ | ✅ | 保留 | +| 备忘录/任务调度 | ✅ | ✅ | 保留 | +| 扩展技能系统 | ✅ | ✅ | 只保留 Markdown 技能,移除可执行技能 | +| 提示词系统 | ✅ | ✅ | 保留(identity/soul 更新) | +| 对话压缩 | ✅ | ✅ | 保留 | +| SOCKS5 代理 | ✅ | ✅ | 保留(API/网页访问) | +| --- | --- | --- | --- | +| `read_file` 读文件 | ✅ | ❌ | 移除,防止任意文件读取(路径遍历 P1-1) | +| `write_file` 写文件 | ✅ | ❌ | 移除,防止任意文件写入 | +| `list_files` 列目录 | ✅ | ❌ | 移除 | +| `run_command` 命令执行 | ✅ | ❌ | **完全移除**,P0-1 根源 | +| `simple_web` 网页访问 | ✅ | ✅ | 需要 SSRF 防护(内网 IP 过滤) | +| `create_skill` 创建技能 | ✅ | ❌ | 移除(会创建文件) | +| `install_skill` 安装技能 | ✅ | ❌ | 移除(会安装可执行) | +| `update_identity` / `update_soul` | ✅ | ✅ | 保留,仅修改提示词文件 | +| --- | --- | --- | --- | + +## 需要添加的安全措施 + +### 1. WebSocket 认证(P0-2 修复) +- 绑定地址强制改为 `127.0.0.1`,通过 gateway/WAF 反向代理接入 +- 握手阶段校验 `token` 查询参数或 `Authorization: Bearer ` +- token 从配置文件读取,环境变量可覆盖 +- 不匹配立即关闭连接,不处理任何消息 + +### 2. SSRF 防护(P1-2 修复) +- `SimpleWebCrawlerTool` 访问网页前解析主机 IP +- 禁止访问内网 IP 段: + - `127.0.0.0/8`(回环) + - `10.0.0.0/8`(私网) + - `172.16.0.0/12`(私网) + - `192.168.0.0/16`(私网) + - `169.254.0.0/16`(链路本地) + - `0.0.0.0/8` + - `100.64.0.0/10`(共享地址) +- 禁止非 HTTP/HTTPS 协议(仅允许 `http://`/`https://`) +- 跟随重定向时重新校验目标地址 + +### 3. 可执行技能完全禁止 +- `SkillManager` 只加载 `.md` Markdown 技能 +- 跳过目录中所有可执行文件(无扩展名或 `.py`/`.sh`) +- `search_skills` 只返回 Markdown 技能 +- 移除 `create_skill`/`install_skill` 工具 + +### 4. 文件操作限制 +- **完全移除** `read_file`/`write_file`/`list_files` +- `update_identity`/`update_soul` 只允许修改 `~/.aibox/prompts/` 下固定文件,不允许路径遍历 +- `check_path_safety` 严格校验文件名,确保在允许目录内 + +### 5. 频率限制(P2-1 修复) +- 每个连接/每个 token 每分钟限制消息轮数(默认 30 轮/分钟) +- 超限返回 `error: rate limit exceeded` + +### 6. 其他加固 +- 启动时强制校验 `DEEPSEEK_API_KEY` 配置,无效直接退出(P1-4 修复) +- 错误信息脱敏,不泄露路径/栈追踪(P1-3 修复) +- 日志脱敏,不记录 API Key、token 等敏感信息(P2-6 修复) + +## 代码重构步骤 + +### 第一步:工具层移除危险工具 +1. 在 `DeepSeekChat._register_tools()` 中: + - 删除/注释 `ReadFileTool` + - 删除/注释 `WriteFileTool` + - 删除/注释 `ListFilesTool` + - 删除/注释 `CommandLineTool` + - 删除/注释 `CreateSkillTool` + - 删除/注释 `InstallSkillTool` + - 保留:`SearchSkillsTool`、`ReloadSkillsTool`、`AskUserTool`、`ExpressEmotionTool`、`UpdateIdentityTool`、`UpdateSoulTool`、`SaveMemoryAndEndConversationTool`、`MemoTool`、`MemoryTool`、`SimpleWebCrawlerTool`、`GetCurrentTimeTool`、`CalculatorTool`、`SubAgentTool`、`SetProxyTool`、`GetProxyTool` + +### 第二步:修改 SkillManager 只加载 Markdown 技能 +1. `SkillManager._load_skills()` 中只加载 `.md` 文件 +2. 跳过所有其他扩展名(包括无扩展名) +3. `search_skills` 只返回 Markdown 技能 +4. 移除执行相关代码路径 + +### 第三步:添加 WebSocket 认证 +1. 在 `handle_connection` 握手时检查 token: + - 从 `query` 中获取 `token` 参数,或从 `Authorization: Bearer` 获取 + - 与配置文件中 `auth.token` 比对 + - 不匹配:关闭连接,返回 1008 错误 +2. 配置新增 `auth.token` 项,必填 +3. 默认监听地址改为 `127.0.0.1`(原 `localhost`) + +### 第四步:SimpleWebCrawler 添加 SSRF 防护 +1. 新增 `_is_private_ip(ip)` 函数判断是否内网 +2. 爬取前: + - 解析 URL 获取主机名 + - DNS 解析得到 IP 地址列表 + - 逐个检查,任一命中内网则拒绝 + - 检查协议,只允许 http/https +3. 跟随重定向时重复检查 + +### 第五步:频率限制实现 +1. `WebSocketHandler` 维护 `rate_limit: Dict[connection_id, (last_time, count)]` +2. 每次处理消息前检查计数,超限拒绝 + +### 第六步:update_identity/update_soul 路径加固 +1. 只允许写 `00_IDENTITY.md` 和 `99_SOUL.md` +2. 文件名硬编码,不允许用户指定路径 +3. 写入前 `realpath` 校验,确保在 `prompts_dir` 内 + +### 第七步:其他细节 +1. 启动时强制校验 API Key,不存在立即退出 +2. 错误信息脱敏,服务端记录详细错误,客户端只返回通用错误 +3. 日志脱敏,不打印 API Key/token + +## 配置文件变化(`config.json`) + +```json +{ + // ... 原有配置不变 ... + "auth": { + "token": "your-secret-token-here", // 新增 必填 + "enabled": true // 新增 默认 true + }, + "rate_limit": { + "enabled": true, // 新增 默认 true + "max_requests_per_minute": 30 // 新增 默认 30 + } +} +``` + +## 部署说明 + +开放版必须通过 gateway/WAF 反向代理接入: +``` +Client → WAF (TLS 终止) → gateway (认证) → aibox (127.0.0.1:8765) +``` +- aibox 只监听 `127.0.0.1`,不直接暴露 +- gateway 完成 token/JWT 认证 +- WAF 处理 TLS 和规则过滤 + +## 安全验证点 + +发布前必须验证: + +- [ ] `run_command` 工具不存在,无法调用 +- [ ] `read_file`/`write_file`/`list_files` 不存在 +- [ ] `create_skill`/`install_skill` 不存在 +- [ ] 无 token 连接被拒绝 +- [ ] 错误 token 连接被拒绝 +- [ ] 正确 token 可连接 +- [ ] 内网 URL 爬取被拒绝(SSRF 防护) +- [ ] 超过频率限制被拒绝 +- [ ] 启动时无 API Key 直接退出 +- [ ] `update_identity`/`update_soul` 只能写固定文件,无法遍历 +- [ ] 只有 `.md` 技能被加载 + +## 发布路线图 + +| 阶段 | 任务 | 状态 | +|------|------|------| +| 阶段 1 | 工具裁剪、SkillManager 修改 | ⏸ 待开始 | +| 阶段 2 | WebSocket 认证实现 | ⏸ 待开始 | +| 阶段 3 | SSRF 防护(SimpleWebCrawler) | ⏸ 待开始 | +| 阶段 4 | 频率限制、启动校验、错误脱敏 | ⏸ 待开始 | +| 阶段 5 | 路径加固(update_identity/update_soul) | ⏸ 待开始 | +| 阶段 6 | 安全验证测试 | ⏸ 待开始 | +| 阶段 7 | 更新 README、更新文档 | ⏸ 待开始 | + +## 风险与约束 + +- 阉割后功能减少,但安全性大大提高,可公开部署使用 +- 仍保留 AI 对话、记忆、任务调度、子代理、扩展 Markdown 技能等核心特性 +- 用户仍可通过 AI 能力总结、分析、创作,只是无法直接操作服务器文件和执行命令 +- 若需要文件操作,建议通过 gateway 提供受限 API,不开放给 aibox 直接操作 \ No newline at end of file diff --git a/docs/security/libth_SECURITY_AUDIT_20260804.md b/docs/security/libth_SECURITY_AUDIT_20260804.md new file mode 100644 index 0000000..54e789e --- /dev/null +++ b/docs/security/libth_SECURITY_AUDIT_20260804.md @@ -0,0 +1,92 @@ +# libth 安全审计报告 + +**审计日期**: 2026-08-04 +**审计范围**: `libth/inc/us/`(`Thread.hpp`、`Fsm.hpp`、`ITask.hpp`、`Cv_wait.hpp`)、`libth/src/Thread.cpp` +**代码版本**: dev_qing 分支 + +--- + +## 修复进度 + +| 优先级 | 总数 | 已修复 | 部分修复 | 未修复 | +|--------|------|--------|----------|--------| +| P0 | 0 | 0 | 0 | 0 | +| P1 | 1 | 0 | 0 | 1 | +| P2 | 4 | 0 | 0 | 4 | +| P3 | 2 | 0 | 0 | 2 | +| **合计** | **7** | **0** | **0** | **7** | + +> 说明:libth 为底层基础线程库,无对外网络攻击面,风险集中在**同步原语缺失超时导致的潜在永久阻塞(DoS)**与**内存/生命周期健壮性**。以下问题均为设计层面缺陷,源码中已有 FIXME/README 标注,尚未修复。 + +--- + +## 🟠 P1 — 高危问题 + +| # | 问题 | 状态 | 说明 | +|---|------|------|------| +| P1-1 | `Cv_wait` 无超时机制,可永久阻塞 | ❌ 未修复 | `Cv_wait.hpp` L21-25 `Wait()` 依赖 `cv.wait(lock, [this]{return flag;})`,无超时与超时中断。若 `WakeCv()` 永不触发(如等待的信号/事件丢失),调用线程将**永久阻塞**。被 `core` 的 `main.cpp` 直接使用,一旦误用即造成服务线程卡死(DoS)。源码 FIXME 已标注"仅支持单次等待-唤醒、可能导致永久堵塞,应改用 `std::future`/`std::promise`" | + +--- + +## 🟡 P2 — 中危问题 + +| # | 问题 | 状态 | 说明 | +|---|------|------|------| +| P2-1 | `Thread::suspend()` 无超时 | ❌ 未修复 | `Thread.cpp` L33-35 透传 `Fsm::suspend()`(`Fsm.hpp` L61-65),阻塞等待状态变更却无超时。若其他线程永不调用 `set()`/`run()`/`stop()`/`shut()`,当前线程永久挂起,造成线程资源耗尽 | +| P2-2 | 异常回退后未释放资源,可能内存泄漏 | ❌ 未修复 | `Thread.cpp` L132-135 `catch(std::exception)` 后仅 `stop()` 回退 STOP,不触发清理。若 `LoopEvent` 在循环中反复抛异常,会反复进入 STOP→START→RUNNING 循环,每次泄漏部分资源,长期运行内存持续增长。README 已注明"catch 后继续运行可能造成内存泄漏" | +| P2-3 | `wake()`/`WaitStart()` 死锁约束脆弱 | ❌ 未修复 | `Thread.cpp` L87-92 `WaitStart()` 先持有 `mtx_th` 再调 `wake()`,而 `wake()`(L38-45)内部禁止再次加锁。该约束依赖调用方自觉遵守,一旦在 `WakeEvent` 回调中误加锁即死锁,缺少防御性校验(如 `std::recursive_mutex` 或断言) | +| P2-4 | `WaitStart()` 存在潜在死锁/异常风险 | ❌ 未修复 | `Thread.hpp` L113 注释"据说该函数有问题可能导致异常"。`WaitStart()` 在 `wake()` 后置 `ready=false` 再 `cv.wait`,若 `WakeEvent` 中未调用 `run()`/`stop()`/`shut()`,`ready` 永不置位,调用线程永久阻塞 | + +--- + +## 🔵 P3 — 低危问题 + +| # | 问题 | 状态 | 说明 | +|---|------|------|------| +| P3-1 | `Activate()` 重复调用静默返回 | ❌ 未修复 | `Thread.cpp` L80 若 `th` 已存在直接 `return`,不报错也不提示。调用方误以为重复激活有效,实际第二个线程未创建,状态不一致且无任何反馈 | +| P3-2 | `Cv_wait` 复用后行为异常 | ❌ 未修复 | `Cv_wait.hpp` L19 `flag` 非原子且复用后残留 `true`,导致后续 `Wait()` 立即返回而不阻塞。README 已注明"仅支持单次使用,复用导致 `Wait()` 不阻塞" | + +--- + +## ✅ 已有安全机制 + +1. ✅ **线程安全状态机**: `Fsm` 用 `std::mutex` 保护状态读写(`Fsm.hpp` L31-34、L50-57),避免并发设置状态的数据竞争 +2. ✅ **两阶段构造防竞态**: `Activate()` 与构造函数分离(`Thread.hpp` L110),避免 `std::thread` 在对象构造完成前访问未初始化成员 +3. ✅ **异常捕获防进程崩溃**: `main()` 循环捕获 `std::exception` 回退 STOP(`Thread.cpp` L108-135),单线程异常不会拖垮整个进程 +4. ✅ **RAII 资源管理**: 使用 `std::unique_ptr`、`std::unique_ptr`(`Thread.hpp` L128-146),避免裸指针管理 +5. ✅ **复制构造禁用**: `Thread(const Thread&) = delete`(`Thread.hpp` L41),防止线程对象被误拷贝造成双重管理 +6. ✅ **回调组合优于继承**: 持 `std::function` 回调(`Thread.hpp` L24),接口收敛、避免子类化的状态泄漏 + +--- + +## 修复路线图 + +### 短期(72小时) +- [ ] P1-1 `Cv_wait` 增加超时等待(`wait_for` + 超时返回),或按源码 FIXME 改用 `std::future`/`std::promise` +- [ ] P2-1 `Fsm::suspend()`/`Thread::suspend()` 增加超时参数,避免永久挂起 + +### 中期 +- [ ] P2-2 异常回退时统一触发 `ClearEvent` 清理,避免长期运行内存泄漏 +- [ ] P2-3 将 `wake()` 的锁约束改为可防御形式(如 `std::recursive_mutex` 或加锁断言诊断) +- [ ] P2-4 `WaitStart()` 增加超时与状态校验,避免 `WakeEvent` 未推进状态时永久阻塞 + +### 长期 +- [ ] P3-1 `Activate()` 重复调用改为抛异常或返回错误码,避免静默失败 +- [ ] P3-2 `Cv_wait` 的 `flag` 改为原子变量并支持复用,或明确废弃并替换 + +--- + +## 代码位置索引 + +| 功能 | 文件 | 行号 | +|------|------|------| +| `Cv_wait::Wait()` 无超时 | `inc/us/Cv_wait.hpp` | L21-25 | +| `Cv_wait::flag` 非原子/复用残留 | `inc/us/Cv_wait.hpp` | L18-19 | +| `Fsm::suspend()` 无超时 | `inc/us/Fsm.hpp` | L61-65 | +| `Thread::suspend()` 透传 | `src/Thread.cpp` | L33-35 | +| `Thread::WaitStart()` 唤醒等待 | `src/Thread.cpp` | L87-92 | +| `Thread::wake()` 禁止加锁 | `src/Thread.cpp` | L38-45 | +| `Thread::Activate()` 重复调用静默返回 | `src/Thread.cpp` | L76-85 | +| `Thread::main()` 异常回退 | `src/Thread.cpp` | L103-138 | +| `Thread` 复制构造禁用 | `inc/us/Thread.hpp` | L41 | +| `Thread` 两阶段构造 | `inc/us/Thread.hpp` | L110 | \ No newline at end of file diff --git a/libth/README.md b/libth/README.md index 33a0cc0..26569e7 100644 --- a/libth/README.md +++ b/libth/README.md @@ -194,6 +194,10 @@ int main() { - `Activate()` 不可重复调用(内部有空指针检查),但不会报错,静默返回。 - 析构函数为虚函数,允许安全的多态删除。 +## 安全审计 + +本模块安全审计文档见 `../docs/security/libth_SECURITY_AUDIT_20260804.md`,包含已知设计缺陷与修复路线图。 + ## 许可证 MIT License