Files
dstalk/agents/audits/W13.1-anthropic-audit.md
XiuChengWu 47082376ef
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
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>
2026-05-27 09:32:13 +08:00

257 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# W13.1 Anthropic Plugin Audit
**Auditor**: 杨帆 (architect-yang)
**Date**: 2026-05-27
**File**: plugins/anthropic/src/anthropic_plugin.cpp (497 行)
**Wave Coverage**: W2.2 (仅 secure_zero 追加,全文从未审计)
---
## 文件总览
| 函数 | 行号 | 类型 | 用途 |
|------|------|------|------|
| `secure_zero` | 34-37 | 静态辅助 | volatile 写零擦除敏感数据 |
| `extract_host_port` | 42-62 | 静态辅助 | URL 解析 host/port/target |
| `build_headers_json` | 67-73 | 静态辅助 | 构建 Anthropic headers JSON (含 api_key) |
| `build_request_json` | 78-124 | 静态辅助 | 构建 Messages API 请求体 |
| `parse_response` | 129-195 | 静态辅助 | 解析非流式 JSON 响应 |
| `parse_sse_data` | 203-238 | 静态辅助 | 解析 SSE data 行 (Anthropic 格式) |
| **`my_configure`** | **243-261** | **vtable** | AI 服务 configure |
| **`my_chat`** | **266-306** | **vtable** | 非流式聊天 |
| **`my_chat_stream`** | **348-428** | **vtable** | 流式聊天 (SSE) |
| `sse_line_callback` | 321-346 | 回调 (C fn ptr) | SSE 行回调,传给 HTTP 服务 |
| **`my_free_result`** | **433-439** | **vtable** | 释放 chat_result 字段 |
| **`on_init`** | **454-468** | **生命周期** | 插件初始化 |
| **`on_shutdown`** | **470-478** | **生命周期** | 插件销毁 |
| `dstalk_plugin_init` | 494-497 | **导出入口** | 返回插件描述符 |
**vtable 绑定**: `g_service` (L444-449) 绑定 configure/chat/chat_stream/free_result。
**全局状态**: `g_host` (L14), `g_http` (L15), `g_config` (L16), `g_cfg` (L29)。
---
## 1. 审计维度: §5 + §8 异常安全
**规则 (§8.1)**: 所有 C ABI 导出函数on_init/on_shutdown/on_event + vtable 函数指针)必须 try/catch 包裹。
### 发现 1.1 — [H] 6 个 C ABI 函数零 try/catch 保护
以下所有通过 vtable 或 `dstalk_plugin_info_t` 暴露给 host 的函数,内部使用 `std::string` / `boost::json` / STL但**无一包裹 try/catch**
| 函数 | 行号 | 使用的可抛类型 | 风险 |
|------|------|---------------|------|
| `my_configure` | L243-261 | `std::string::operator=` (L247-250) | `std::bad_alloc``std::terminate()` |
| `my_chat` | L266-306 | `std::string` x5 (L279-286) + `boost::json` (build 系列) | 同上 |
| `my_chat_stream` | L348-428 | `std::string` x4 (L361-368) + `boost::json` | 同上 |
| `on_init` | L454-468 | `g_host->log` 可变参数 + query_service | 低概率但可能 |
| `on_shutdown` | L470-478 | `std::string::clear()` (noexcept) + `g_host->log` | 极低概率 |
| `sse_line_callback` | L321-346 | `std::string line_str(line)` (L326) | `std::bad_alloc` 抛向调用方 (网络插件) |
**违反 §8.1 条目 1+2+4**: on_init/on_shutdown + vtable 函数指针 + register_service 注册的函数。
**代价**: 任何 OOM 或 STL 异常 → 穿越 C 函数指针边界 → `std::terminate()` → 进程崩溃。
W11.1 对 context_plugin 发现了完全相同的模式 (trim_impl)本插件问题范围更广6 个函数 vs 1 个)。
**对比 deepseek_plugin.cpp**: deepseek 同函数**同样零 try/catch**my_configure L238-256, my_chat L261-301, my_chat_stream L337-417, on_init L443-457, sse_line_callback L317-335说明此反模式是项目级共性问题。
### 发现 1.2 — [M] sse_line_callback 无保护但可能被网络插件兜底
L326 `std::string line_str(line)` 可抛 `std::bad_alloc`。回调通过 C 函数指针 `dstalk_stream_cb` 传入网络插件的 `post_stream`。根据 security-logging.md 对 network_plugin 的描述L280-282 有 `catch (std::exception& e)`),网络插件的 do_post_stream 可能兜底捕获。但依赖调用方的异常保护是脆弱假设——若网络插件变更或不同版本,此防护消失。
---
## 2. 审计维度: §9 字符串返回值生命周期
**规则 (§9)**: vtable 中返回 `const char*` 的函数必须符合模式 A拥有权转移host 堆分配 + 调用方 host->free或模式 B静态生命周期
### 发现 2.1 — [L] 临时 std::string + c_str() + strdup 的脆弱模式
L405-406:
```c
r.error = g_host->strdup(("HTTP " + std::to_string(status_code)).c_str());
```
临时 `std::string` 在完整表达式结束前有效,`strdup` 在其析构前完成复制。**当前安全**,但重构时若分离 `c_str()``strdup` 调用会导致悬垂指针。Deepseek 同位置 (L393-395) 使用相同模式。
### 合规确认
| 检查项 | 状态 | 证据 |
|--------|------|------|
| chat_result 字段全部经 strdup 分配 | PASS | L144, L146, L150, L168, L176, L179, L186, L191, L275, L296, L357, L396-397, L403, L405-406, L418, L424 |
| free_result 全部经 host->free 释放 | PASS | L436-438 |
| 无锁外 c_str() 返回 | PASS | 插件无 mutex/lock |
| 无栈上 buffer 返回 | PASS | 全部 strdup 复制 |
| 无 `std::string{...}.c_str()` 裸返回 | PASS | 临时对象均在 strdup 前存活 |
---
## 3. 审计维度: §2 + §3 跨 DLL 堆纪律
**规则 (§3.2)**: 严禁直接调用 malloc/free/strdup/new/delete 处理跨 DLL 边界数据。
### 发现 3.1 — [H] response_body 泄漏my_chat 错误路径未释放
L291-297 (`my_chat`):
```c
int ret = g_http->post_json(..., &response_body, &status_code);
if (ret != 0) {
r.error = g_host->strdup("http request failed");
return r; // ← response_body 未释放!
}
```
成功路径 (L302-304) 正确释放了 `response_body`,但 `ret != 0` 错误路径泄漏。
**对比**: `my_chat_stream` (L408, L414) 在**所有路径**正确释放 `response_body``my_chat` 应与 `my_chat_stream` 保持一致。
**deepseek_plugin.cpp 同位置** (L286-293) 有相同泄漏——此为项目级模式 bug。
### 合规确认
| 检查项 | 状态 | 证据 |
|--------|------|------|
| 无裸 malloc/free | PASS | 全文 0 处 |
| 无裸 strdup | PASS | 全文 0 处 |
| 无裸 new/delete | PASS | 全文 0 处 |
| 跨边界分配均经 host->strdup | PASS | 全部 chat_result 字段 |
| 跨边界释放均经 host->free | PASS | L302-304, L408, L414, L436-438 |
| 插件内部 std::string 不出边界 | PASS | 仅用于局部逻辑 |
---
## 4. 审计维度: §6 atomic 回调 + 线程安全
**规则 (§6.1)**: 诊断回调用 `std::atomic` + acquire/release。服务注册表/事件总线均有内部锁。
### 发现 4.1 — [H] 全局指针无同步保护 (g_host / g_http / g_config)
| 变量 | 写点 (on_shutdown) | 读点 (服务函数) | 同步机制 |
|------|-------------------|-----------------|----------|
| `g_host` | L477 `= nullptr` | L255, L274, L296, L357, L436 等 | **无** |
| `g_http` | L475 `= nullptr` | L274, L356 | **无** |
| `g_config` | L476 `= nullptr` | (未读取,死变量) | **无** |
**Race 场景**: 线程 T1 在 `my_chat()` L274 读取 `g_http`(非 null线程 T2 在 `on_shutdown()` L475 写入 `nullptr`。T1 随后 L291 调用 `g_http->post_json(...)`**空指针解引用**
**缓解因素**: host 应保证 shutdown 前无 in-flight 调用,但 ABI 未显式保证。与 W11.1 发现的 context_plugin Race A 问题同根。
### 发现 4.2 — [L] g_config 死变量
L16 声明 `g_config`L458 赋值L476 置 null。全文无任何读取点。不造成泄漏但暗示设计与实现脱节。
### 发现 4.3 — [PASS] 流式回调路径无需 atomic
`StreamContext` (L312-318) 在 `my_chat_stream` 栈上创建 (L370),回调 `sse_line_callback``post_stream` 同步调用期间使用,无多线程竞争。
---
## 5. 审计维度: api_key 安全
### 发现 5.1 — [PASS] on_shutdown 正确 secure_zero
L473-474:
```c
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
g_cfg.api_key.clear();
```
W2.2 修复已验证生效:先 volatile 零覆盖,再 clear 释放。`secure_zero` (L34-37) 使用 `volatile char*` 写入循环,是标准可移植的编译器优化防护手段。
### 发现 5.2 — [PASS] 日志不泄漏 api_key
W9.3 已审计确认:`my_configure` L255-258 的日志调用有意排除 `api_key`,仅输出 model/base_url/max_tokens/temperature。`build_headers_json` (L67-73) 构建的 headers JSON 仅通过内存传递至 HTTP 请求,不进入日志管道。
### 发现 5.3 — [L] 堆内存残留
`build_headers_json` 返回的 `std::string``x-api-key: sk-...`,在 `my_chat`/`my_chat_stream` 栈上存活至函数返回。RAII 析构释放内存但不擦除。攻击者读取进程堆可发现 api_key 残留。这是所有内存中处理凭证的固有限制非本插件特有secure_zero 仅覆盖 `g_cfg.api_key` 主副本。
---
## 6. 审计维度: 错误处理路径
### 发现 6.1 — [L] my_chat 对 post_json 错误仅返回通用消息
L291-297: `ret != 0` 时仅返回 "http request failed",未利用 `status_code` 区分超时/SSL/DNS 错误。`my_chat_stream` (L402-406) 有更细粒度的错误消息(区分 `status_code <= 0` 为 "transport error")。
### 发现 6.2 — [PASS] 空响应/空 body 不崩溃
- `parse_response` L136/L158: `body ? body : "{}"` 保护 null 输入
- L177-179: content 数组为空 → "empty response"
- `my_chat_stream` L416-418: `accumulated.empty() && !saw_data_line` → "no content received"
- `parse_sse_data` L234: catch(...) 静默忽略 JSON 解析失败
### 发现 6.3 — [PASS] HTTP 错误码正确传播
`parse_response` L134-154: 非 2xx 状态码提取 error.message 或退化为 "HTTP NNN"。
`my_chat_stream` L388-411: 同模式,附加 `status_code <= 0` 的传输错误处理。
---
## 7. 审计维度: 资源生命周期
### 发现 7.1 — [PASS] 流式中断资源正确释放
- `StreamContext` (L312-318) 栈分配RAII 保证析构
- `response_body``my_chat_stream` L408 + L414 双路径释放
- `user_cb` 返回 0 时L340`sse_line_callback` 返回 0`post_stream` 应据此停止——无孤儿连接
- 流结束后 `ctx.accumulated` 正确取出赋值 L424
### 发现 7.2 — [L] my_chat_stream 忽略 post_stream 返回值
L379-383: `int ret = g_http->post_stream(...)` 的结果未被检查。`my_chat` 检查了 `ret`L295`my_chat_stream` 仅在 status_code 上做判断。若 `ret != 0``status_code == 0`(连接完全失败),会落入 L402 `status_code <= 0` → "transport error" 分支——实际可工作,但不一致。
---
## 8. 补充发现
### 8.1 — [L] Anthropic 特有: tool_use 块被静默忽略
`parse_response` L163-173 仅提取 `type == "text"` 的 content block。Anthropic API 在 tool-use 场景下返回 `type == "tool_use"` 的块,其内容不会被提取到 `r.content``r.tool_calls_json` 始终被设为 `nullptr` (L153, L171, L182, L187, L193)。调用方期待 tool_calls 时将收到空 content + 空 tool_calls——静默数据丢失。不过 `dstalk_ai_service_t.chat` 签名设有 `tools_json` 入参但未在 Anthropic 请求体中设置(对比 deepseek L129 有 `root["tools"] = json::parse(tools_json)`),因此当前工具调用完全不可用。
### 8.2 — [L] system 消息合并到顶层字段
L92-97: Anthropic API 要求 system 为顶层字段(非 messages 数组元素),插件正确实现了此分离。但多段 system 消息用 `"\n\n"` 拼接 (L95),跨消息语义边界可能模糊。
---
## TOP 3 严重问题
### 发现 1 — [H] 6 个 C ABI 函数零 try/catch (§8 违反正面清单)
**行号**: L243 (my_configure), L266 (my_chat), L348 (my_chat_stream), L321 (sse_line_callback), L454 (on_init), L470 (on_shutdown)
**问题**: 全部使用 std::string/boost::json 但无异常保护。任何 std::bad_alloc → std::terminate()。
**修复**: 每个函数外层加 try { ... } catch (const std::exception& e) { g_host->log(ERROR, ...); return 错误码; } catch (...) { return 错误码; }
### 发现 2 — [H] response_body 泄漏 + 全局指针无同步
**行号**: L295-297 (泄漏), L14-L16 vs L475-L477 (竞态)
**问题**: (a) my_chat 在 ret!=0 时未释放 response_bodymy_chat_stream 正确);(b) g_host/g_http/g_config 无原子/互斥保护on_shutdown 与 服务函数并发可空指针解引用。
**修复**: (a) 在 L296 前加 `if (response_body) g_host->free(response_body);`(b) g_host/g_http 用 `std::atomic` 或文档化单线程约定。
### 发现 3 — [M] Anthropic tool_use 响应静默丢弃
**行号**: L129-195 (parse_response), L78-124 (build_request_json)
**问题**: (a) build_request_json 未将 tools_json 参数写入请求体(对比 deepseek L128-130(b) parse_response 仅提取 text blocktool_use 块被忽略tool_calls_json 始终 nullptr。用户调用工具时得到空响应无错误提示。
---
## 整体评级: C
| 维度 | 评级 | 理由 |
|------|------|------|
| §8 异常安全 | **F** | 6 个函数零 try/catch——mandatory 规则完全未满足 |
| §2/§3 跨 DLL 堆 | **A** | 无裸 malloc/free/strdup全部经 host API |
| §9 字符串生命周期 | **B+** | 全部 strdup 合规,一处临时+strdup 脆弱模式 |
| §6 线程安全 | **C** | 全局指针无同步,与 W11.1 同根问题 |
| api_key 安全 | **A** | secure_zero 生效,日志无泄漏 |
| 错误处理 | **C+** | 空响应不崩溃,但 response_body 泄漏+返回值忽略 |
| 资源生命周期 | **B+** | 流式路径 RAII 干净,仅 my_chat 泄漏点 |
| **综合** | **C** | 堆纪律意外干净,但异常安全零覆盖 + 全局指针竞态 + response_body 泄漏 将评级拉至 C |
**总评**: anthropic_plugin 在跨 DLL 堆纪律方面表现好(与 context_plugin 一致),说明编码者遵循了正确的内存管理模式。但 **§8 异常安全是强制性要求,当前零覆盖在最坏情况下直接导致进程崩溃**。response_body 泄漏和全局指针竞态进一步降低可靠性。tool_use 静默丢弃虽非安全/稳定性问题,但构成功能缺陷。评级 C建议在下一 Wave 优先修复发现 1 和发现 2。
---
## 审计元数据
- **审计标准**: plugin-abi.md v1.1 (含 §8 异常安全 + §9 字符串生命周期W12.6 追加)
- **参考审计风格**: agents/audits/W11.1-context-audit.md
- **安全日志参考**: docs/explanation/security-logging.md (W9.3)
- **对比文件**: plugins/deepseek/src/deepseek_plugin.cpp (仅参考,不审计)
- **不修改文件**: anthropic_plugin.cpp (审计只读)