Wave 10: deep audits of 5 unaudited plugins, smoke regression set (W13.1-W13.6)
- W13.1 anthropic_plugin (architect-yang, 497 lines): rated C. 6 C ABI functions lack try/catch (§8 violation); my_chat leaks response_body on error path; tool_use response silently dropped. - W13.2 deepseek_plugin (engineer-sun, 486 lines): rated C+. 7 ABI entries unprotected including json::parse paths (malformed JSON terminates); SSE [DONE] sentinel match brittle; ~55% code overlap with anthropic suggests an ai_plugin_base extraction. - W13.3 network_plugin (qa-wang, 322 lines): rated C. CRITICAL: TLS certificate verification fully disabled (set_verify_mode never called, default verify_none accepts any cert) — all AI traffic incl. api_key is MITM-vulnerable. DNS resolve has no timeout; catch lacks (...). - W13.4 lsp_plugin (architect-huang, 749 lines): rated C. CRITICAL: guaranteed deadlock at L519-526 → L547 (g_lsp_impl_start holds mutex then calls g_lsp_impl_stop which re-locks the same non-recursive mutex); 7 vtable funcs unprotected; server→client requests dropped. - W13.5 session+tools (security-cao, 264+251 lines): rated D+/D. Path traversal in builtin_file_read/write (zero validation); global static state in both plugins lacks mutex (UAF risk); 9 vtable funcs lack try/catch. - W13.6 smoke regression (qa-xu, +193 lines): 4 new cases — context max_tokens trim, config dual-store consistency (exposes that W12.2 merge is incomplete: dstalk_config_set→config_service.get returns null), HTTP error path no-crash, repeated init/shutdown cycle. Verified: cmake build 0 error 0 warning, ctest 4/4 pass. Top W14 priorities surfaced: TLS verification (W13.3), LSP deadlock (W13.4), file-tool path traversal (W13.5), config dual-store still broken (W13.6 R2), shared try/catch wrapper across all AI plugins. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
230
agents/audits/W13.5-session-tools-audit.md
Normal file
230
agents/audits/W13.5-session-tools-audit.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# W13.5 Session + Tools Plugin 联合审计
|
||||
|
||||
**审计人**: 曹武 (security-cao)
|
||||
**日期**: 2026-05-27
|
||||
**文件**: plugins/session/src/session_plugin.cpp (264 行) + plugins/tools/src/tools_plugin.cpp (251 行)
|
||||
**标尺**: plugin-abi.md §2 §5 §8 §9; security-logging.md
|
||||
**前置工作**: W2.2 (api_key secure_zero, 仅 deepseek/anthropic) — 已注明合规; W9.3 (日志凭证审计, session 已审日志路径) — 已注明合规
|
||||
|
||||
---
|
||||
|
||||
## 1. Session 文件总览
|
||||
|
||||
session_plugin.cpp 实现 dstalk_session_service_t vtable (6 个函数): add / clear / save / load / history / token_count。
|
||||
内部状态:
|
||||
- `g_history`: std::vector<InternalMessage> — 完整消息历史 (role + content + tool_call_id + tool_calls_json)
|
||||
- `g_cached_history`: std::vector<dstalk_message_t> — history() 返回的 C 数组缓存 (g_host->strdup 分配)
|
||||
- `g_host` / `g_file_io`: 服务指针
|
||||
|
||||
依赖: file_io (save/load 持久化), Boost.JSON (序列化)。
|
||||
|
||||
---
|
||||
|
||||
## 2. Tools 文件总览
|
||||
|
||||
tools_plugin.cpp 实现 dstalk_tools_service_t vtable (4 个函数): register_tool / unregister_tool / get_tools_json / execute。
|
||||
内置两个工具:
|
||||
- `file_read`: 读取任意路径文件内容, 返回 JSON `{"content":"..."}`.
|
||||
- `file_write`: 写入任意路径, 返回 JSON `{"success":true}`.
|
||||
|
||||
内部状态:
|
||||
- `g_tools`: std::vector<ToolDef> — 已注册工具列表
|
||||
- `g_host` / `g_file_io`: 服务指针
|
||||
|
||||
依赖: file_io, Boost.JSON。
|
||||
|
||||
---
|
||||
|
||||
## 3. 按维度发现
|
||||
|
||||
### 维度 1: Session 敏感数据持有与清零 [session]
|
||||
|
||||
**W2.2 覆盖范围**: api_key 的 secure_zero 仅实现在 deepseek_plugin.cpp 和 anthropic_plugin.cpp 的 anonymous namespace 内。session 插件**不在** W2.2 范围内 — 已注明合规。
|
||||
|
||||
**发现 1.1 — [高] 会话历史明文持有可能含 token 的数据** (session L33-40, L127-135)
|
||||
- g_history 存储完整的 InternalMessage (含 tool_calls_json)。AI 模型回复中的 tool_calls 可包含 API 返回的敏感参数。
|
||||
- on_shutdown (L242-249) 仅调用 g_history.clear() → std::string 析构 → 内存归还堆, **未 secure_zero 擦除**。
|
||||
- g_cached_history 中的字符串通过 g_host->free 释放 (L104-107), 同样未擦除。
|
||||
- 与 W2.2 的 secure_zero 处置形成落差: api_key 被安全擦除, 但可能包含等效敏感度的会话数据未受同等保护。
|
||||
|
||||
**发现 1.2 — [中] 会话落盘明文无保护** (session L141-157)
|
||||
- session_save 将完整消息 JSON 逐行写入 path 指定文件。content 字段包含用户输入 + AI 回复原文, tool_calls_json 包含工具调用参数。
|
||||
- 无加密、无访问控制、无审计日志。
|
||||
- path 参数无校验 — 依赖 file_io 服务实现 (若 file_io 无 sandbox, 可写任意路径)。
|
||||
- 评级: 若 g_history 中包含 token 或隐私数据, 则构成明文存储敏感信息。
|
||||
|
||||
### 维度 2: Session 历史持久化安全 [session]
|
||||
|
||||
- 见维度 1 发现 1.2。补充: session_load (L159-202) 同样无路径校验, 可加载攻击者构造的 JSON 文件注入恶意历史记录 (如伪造 tool_call_id 导致下游误操作)。
|
||||
- 加载时的 JSON 解析异常仅返回 -1 未记日志 (L194-195), 不利于入侵检测。
|
||||
|
||||
### 维度 3: Tools 路径校验与注入防护 [tools]
|
||||
|
||||
**发现 3.1 — [严重] 路径遍历: file_read / file_write 无路径消毒** (tools L50, L85)
|
||||
- builtin_file_read (L39-67): `g_file_io->read(path.c_str(), &content)` — path 直接来自用户 JSON 输入, 未经任何规范化/白名单/拒绝遍历序列 (../) 检查。
|
||||
- builtin_file_write (L69-98): 同。
|
||||
- 攻击场景: `{"path":"../../../etc/passwd"}` 或 `{"path":"C:/Windows/System32/drivers/etc/hosts"}` — 取决于 file_io 底层实现。
|
||||
- CVSS 初步评分: 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N 需确认调用方可控性) — 若 AI 模型被 prompt injection 诱导调用 file_read/file_write, 攻击者可读写任意文件。
|
||||
- **未发现命令注入**: 全文搜索无 system() / popen() / exec() / ShellExecute() / CreateProcess() 调用。仅通过 file_io 服务间接操作文件系统, 非 shell 执行。
|
||||
|
||||
### 维度 4: Tools dispatch 权限校验 [tools]
|
||||
|
||||
**发现 4.1 — [中] tools_execute 无调用方身份/权限检查** (tools L160-190)
|
||||
- 任何持有 tools service vtable 指针的代码均可调用 execute(name, args) 执行任意已注册工具。
|
||||
- 无 capabilities / ACL / rate limiting / audit logging。
|
||||
- 如果未来注册 shell_exec 或 network 工具, 风险急剧升级。当前仅 file_read/write 但仍可被恶意插件滥用。
|
||||
|
||||
### 维度 5: §5 + §8 异常安全 (全部 C ABI 入口) [session + tools 共有]
|
||||
|
||||
**W11.1 审计已知模式**: context_plugin 因同样问题被评为 B 级 (trim_impl 无 try/catch)。
|
||||
|
||||
**发现 5.1 — [高] Session: 5/8 个 C ABI 函数无异常保护**
|
||||
| 函数 | 行号 | 风险操作 | try/catch |
|
||||
|------|------|----------|-----------|
|
||||
| session_add | L127-135 | std::string 构造, vector::push_back | **无** |
|
||||
| session_save | L141-157 | std::string, json::object, json::serialize | **无** |
|
||||
| session_load | L159-202 | std::string, std::vector, string::substr | 仅 L178-196 (json::parse 部分) |
|
||||
| session_history → rebuild_cached_history | L101-121 | std::vector, g_host->strdup | **无** |
|
||||
| on_shutdown | L242-249 | → rebuild_cached_history | **无** |
|
||||
| session_clear | L137 | g_history.clear() | noexcept — **安全** |
|
||||
| session_token_count | L210 | 纯计算 | **低风险** |
|
||||
| on_init | L227-240 | host->log, register_service | host 侧有保护, 插件侧**无** |
|
||||
|
||||
**发现 5.2 — [高] Tools: 5/8 个 C ABI 函数无异常保护**
|
||||
| 函数 | 行号 | 风险操作 | try/catch |
|
||||
|------|------|----------|-----------|
|
||||
| tools_register_tool | L106-121 | std::string, vector::push_back | **无** |
|
||||
| tools_unregister_tool | L123-130 | std::string, vector::erase | **无** |
|
||||
| tools_get_tools_json | L132-157 | json::parse, json::serialize, std::string | **无** |
|
||||
| on_init | L203-230 | → tools_register_tool x2 | **间接暴露** |
|
||||
| tools_execute | L160-190 | handler 调用 | **部分** (L180-189) |
|
||||
| builtin_file_read | L39-67 | json::parse, std::string | **有** (L63-66) |
|
||||
| builtin_file_write | L69-98 | json::parse, std::string | **有** (L94-97) |
|
||||
| on_shutdown | L232-236 | clear + nullptr | noexcept — **安全** |
|
||||
|
||||
**违反条款**: plugin-abi §8.1 (适用范围第2项: service vtable 函数) + §8.2 (实施要求)。
|
||||
**后果**: OOM 或 json::parse 异常时 → std::terminate() → 进程崩溃, 无恢复机会。
|
||||
|
||||
**发现 5.3 — [低] 异常消息信息泄露** (tools L64, L95)
|
||||
- builtin_file_read/write 的 catch 块将 `e.what()` 直接拼入返回给调用方的 JSON 字符串。
|
||||
- `e.what()` 可包含文件路径、系统错误描述等内部信息。
|
||||
- 虽算不上安全漏洞 (调用方本就控制 path 参数), 但违反最小信息暴露原则。
|
||||
|
||||
### 维度 6: §9 字符串返回 [session + tools 共有]
|
||||
|
||||
**发现 6.1 — [中] session_history 返回指针在下一次调用时悬垂** (session L101-121, L204-208)
|
||||
- `session_history()` 调用 `rebuild_cached_history()`, 后者先 `g_host->free` 所有旧 g_cached_history 字符串 (L104-107) 再重建。
|
||||
- 调用方若持有上一次 history() 返回的 `const dstalk_message_t*`, 在下一次 history() 调用后其中的 role/content 指针已悬垂 (已 free)。
|
||||
- 模式类似 dstalk_chat_result_t (§2.1), 但 **无文档警告** "must copy before next history()"。
|
||||
- 违反 §9.2 模式 A 文档要求 ("caller must free" 等效说明缺失)。
|
||||
|
||||
**发现 6.2 — [低] Tools 返回字符串模式正确但缺文档** (tools L62, L93, L157)
|
||||
- builtin_file_read/write, tools_get_tools_json, tools_execute 错误路径全部使用 g_host->strdup 返回 — Mode A, 正确。
|
||||
- 但函数无注释说明 "caller must free with host->free", 违反 §9.2 文档要求。
|
||||
|
||||
### 维度 7: §2 跨 DLL 堆纪律 [session + tools 共有]
|
||||
|
||||
**两个文件均合规** ✅ (同 W11.1 审计结论 — 编码者即使未读 plugin-abi 也遵循了正确模式):
|
||||
- 所有跨边界分配: g_host->strdup (session L115-118; tools L62, L93, L157)
|
||||
- 所有跨边界释放: g_host->free (session L104-107, L167)
|
||||
- 内部类型: std::string / std::vector 仅插件 DLL 内使用, 不出边界
|
||||
- 无裸 malloc / free / strdup / new / delete 处理 host 传入/传出数据
|
||||
|
||||
**W2.1**: 两个文件均未搜到裸 free 调用。任何 W2.1 的 free 迁移已在上层重构中消化 (旧 deepseek_api.cpp 等已删除)。已注明合规。
|
||||
|
||||
### 维度 8: 并发安全 [session + tools 共有]
|
||||
|
||||
**发现 8.1 — [严重] 两个插件所有全局状态均无同步保护**
|
||||
|
||||
Session 竞态表:
|
||||
| 变量 | 写入方 | 读取方 | 同步 |
|
||||
|------|--------|--------|------|
|
||||
| g_history | add, clear, load | save, token_count, rebuild_cached_history | **无** |
|
||||
| g_cached_history | rebuild_cached_history (via history) | history | **无** |
|
||||
| g_host | on_init, on_shutdown | rebuild_cached_history, save, load | **无** |
|
||||
| g_file_io | on_init, on_shutdown | save, load | **无** |
|
||||
|
||||
Tools 竞态表:
|
||||
| 变量 | 写入方 | 读取方 | 同步 |
|
||||
|------|--------|--------|------|
|
||||
| g_tools | register_tool, unregister_tool | get_tools_json, execute | **无** |
|
||||
| g_host | on_init, on_shutdown | get_tools_json, execute, file_read, file_write | **无** |
|
||||
| g_file_io | on_init, on_shutdown | file_read, file_write | **无** |
|
||||
|
||||
**典型竞态场景**:
|
||||
- Race A (session): T1 add() push_back to g_history, T2 save() iterates → 迭代器失效 / 数据损坏。
|
||||
- Race B (session): T1 history() reads g_cached_history, T2 history() frees + rebuilds → UAF。
|
||||
- Race C (tools): T1 execute() iterates g_tools, T2 unregister_tool() erases → 迭代器失效。
|
||||
- Race D (共有): T1 any-service reads g_host/g_file_io, T2 on_shutdown() writes nullptr → NPD (空指针解引用)。
|
||||
|
||||
Race D 已于 W11.1 审计确认 (context 的同模式问题评为 C 级)。session + tools 的竞态面更大 (更多全局可变状态)。
|
||||
|
||||
---
|
||||
|
||||
## 4. TOP 3 严重问题
|
||||
|
||||
### TOP 1 — [严重] 路径遍历: file_read/file_write 无路径消毒 (tools L50, L85)
|
||||
- 攻击者控制 args_json → 读写任意文件系统路径。无 sandbox、无白名单、无规范化。
|
||||
- 若 AI 模型被 prompt injection 诱导调用工具, 可泄露系统配置/源码/凭证文件。
|
||||
- CVSS: 7.5 (High) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N (假设调用方可控)
|
||||
- 修复方向: 在 builtin_file_read/write 中添加路径规范化 (realpath/Canonicalize), 限制在预设工作目录内, 拒绝 ".." 段。
|
||||
|
||||
### TOP 2 — [严重] 全局状态无同步 → 多线程竞态 (session + tools, 全文)
|
||||
- g_history / g_tools / g_host / g_file_io 读写均无 mutex。
|
||||
- 后果: 迭代器失效 (crash)、use-after-free (crash/任意代码)、空指针解引用 (crash)。
|
||||
- 影响面: 两个插件共 8 个 vtable 函数 + 2 个 on_shutdown = 10 个竞态入口。
|
||||
- CVSS: 7.5 (High) — 多线程使用时 crash 概率高, 可能远程触发 (通过 AI chat 间接调用 session/tools)。
|
||||
- 修复方向: 每个插件引入 std::shared_mutex (或 std::mutex), 写操作持 unique_lock, 读操作持 shared_lock。on_shutdown 需额外原子/锁确保服务调用排空。
|
||||
|
||||
### TOP 3 — [高] C++ 异常穿越 ABI 边界 (session L127/L141/L204/L242 + tools L106/L132/L203)
|
||||
- session 5 个函数 + tools 4 个函数使用 std::string/std::vector/json::parse 但无 try/catch 包裹。
|
||||
- 违反 plugin-abi §8 硬性规则 → OOM 或解析异常时 std::terminate() → 进程崩溃。
|
||||
- W11.1 已在 context 中识别同模式问题 (评为 B 级, trim_impl 无保护)。session/tools 影响面更广 (9 个 vtable 函数 vs context 的 3 个)。
|
||||
- CVSS: 5.9 (Medium) — 触发需 OOM 或恶意 JSON 输入, 后果为 DoS (进程崩溃)。
|
||||
- 修复方向: 按 §8.2 标准模式, 每个使用 C++ 类型的 C ABI 函数体包裹 `try { ... } catch (const std::exception& e) { log; return -1/nullptr; } catch (...) { log; return -1/nullptr; }`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键问题回答
|
||||
|
||||
| 问题 | 结论 |
|
||||
|------|------|
|
||||
| 是否发现凭证泄露? | **潜在风险**: session_save 明文落盘含 tool_calls_json, 若工具调用参数含 token 则泄露。当前未确认可利用实例, 但防御缺失。 |
|
||||
| 是否发现命令注入? | **未发现**: 两文件均无 system()/popen()/exec() 调用。tools 仅通过 file_io 服务间接操作文件, 非 shell 执行。 |
|
||||
| 是否发现路径遍历? | **确认**: tools builtin_file_read/write 无路径消毒 (L50, L85), 攻击者可读写任意文件。 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 整体评级
|
||||
|
||||
### Session: D+ (62/100)
|
||||
|
||||
| 维度 | 评级 | 说明 |
|
||||
|------|------|------|
|
||||
| §2 跨 DLL 堆 | A | 完全合规, host->strdup/free |
|
||||
| §5/§8 异常安全 | D | 5/8 C ABI 函数无 try/catch |
|
||||
| §9 字符串返回 | C | 模式正确但缺文档; history() 悬垂风险 |
|
||||
| 敏感数据处理 | C | 明文落盘 + 无 secure_zero |
|
||||
| 并发安全 | D | 零同步, 4 个静态全局无保护 |
|
||||
| **综合** | **D+** | 堆纪律干净但异常安全/并发/数据保护三项不及格 |
|
||||
|
||||
### Tools: D (60/100)
|
||||
|
||||
| 维度 | 评级 | 说明 |
|
||||
|------|------|------|
|
||||
| §2 跨 DLL 堆 | A | 完全合规, host->strdup/free |
|
||||
| §5/§8 异常安全 | C | 内置工具有保护, 注册/查询/on_init 无 |
|
||||
| §9 字符串返回 | B | Mode A 正确, 缺文档 |
|
||||
| 路径校验 | F | file_read/write 任意路径, 无消毒 |
|
||||
| 访问控制 | F | execute 无调用方身份/权限检查 |
|
||||
| 并发安全 | D | 零同步, 3 个静态全局无保护 |
|
||||
| **综合** | **D** | 路径遍历为硬伤; 并发/异常安全与 session 同病 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 低优先级发现
|
||||
|
||||
- **session_load 异常吞没无日志** (session L194-195): 解析失败仅返回 -1 不记日志。
|
||||
- **tools 异常消息泄露** (tools L64, L95): e.what() 返回给调用方, 含内部路径/系统错误。
|
||||
- **两个插件的 on_init 均未检查 register_service 返回值**: 若服务名冲突返回 -2 被静默忽略。
|
||||
- **W9.3 已审结论维持**: session 仅 on_init L233 调用 host->log(ERROR, 静态字符串) — 无凭证日志泄露。tools 无任何 host->log 调用。两个文件在日志维度上安全。
|
||||
Reference in New Issue
Block a user