Files
dstalk/agents/audits/W13.3-network-audit.md
XiuChengWu 6f492489c6
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
W16: close CRITICAL/HIGH findings, integrate metadata gate, complete audit summaries (W16.1-W16.6)
- W16.1 (曹武): F-11.7-1 CLOSED — confirmed W12.4 fix, corrupt binary eliminated
- W16.2 (孙宇): F-11.1-1 FIXED — context_plugin.cpp try/catch on set_max_tokens + on_shutdown
- W16.3 (陈风): F-11.1-2 CLOSED — confirmed W12.1 fix, strdup OOM protection already in place
- W16.4 (胡桐): Integrate check_agents_metadata into refresh_status.py as pre-gate (error→exit 1)
- W16.5 (周岩): Add Findings Summary to W13.3 network audit, register 3 findings
- W16.6 (赵码): Add Findings Summary to W13.1+W13.2 AI audits, register 8 findings (4 already W14-fixed)

Build 0 error, ctest 4/4 pass, metadata check 0 error 0 warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:45:03 +08:00

177 lines
9.0 KiB
Markdown
Raw Permalink 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.3 Network Plugin Audit
**Auditor**: 王测 (qa-wang)
**Date**: 2026-05-27
**File**: plugins/network/src/network_plugin.cpp (322 行)
**Scope**: plugin-abi.md §2 §3 §5 §8 §9, TLS, timeout, concurrency, resources
---
## 1. 跨 DLL 堆纪律 (§2 §3)
**评级: A (完全合规)**
逐行检查:
- L259: `g_host->strdup(result_body.c_str())` — 正确使用 host 堆分配,符 §3.2 模式 A。调用方 `dstalk_free` 释放。
- L40-80 `parse_headers_json`: 内部 `std::string` / `unordered_map`,不跨 DLL 边界。
- L118-123 Lambda capture: 内部 `std::string`,不跨边界。
- 全文零处裸 `malloc`/`free`/`strdup`/`new`/`delete`
唯一跨边界分配 L259 使用 `g_host->strdup`,正确。
## 2. 异常安全 ABI (§5 §8)
**评级: B (基础合规,缺 catch(...))**
- `on_init` (L296-303): 无线程局部 STL 操作,不会抛异常。安全。
- `do_post_stream` (L140-254): 整个 I/O 逻辑包裹在 `try { ... } catch (std::exception& e)` (L251)。所有 `std::string`/`unordered_map`/`std::function` 使用均在保护范围内。
- **缺失**: L251 仅捕获 `std::exception&`,无 `catch(...)` 兜底。符 §8.2 推荐但不符最佳实践§8.2 正例含 `catch(...)`)。
- vtable 函数 `http_post_json` / `http_post_stream` 直接调用 `do_post_stream`,间接享受到保护。不直接暴露异常。
- 若出现非 `std::exception` 派生的异常SEH / 自定义异常),将穿越 ABI → `std::terminate()`
## 3. 字符串返回值生命周期 (§9)
**评级: A**
- L248: `result_body = parser.get().body()` — 复制到本地 `std::string`
- L252-253: `result_body = e.what()``e.what()` 的 C 字符串在 `e` 析构前已复制到 `std::string`,安全。
- L259: `*response_body = g_host->strdup(result_body.c_str())` — 在函数返回前用 host 堆复制,符 §9.2 模式 A拥有权转移。调用方负责释放。
- 无悬垂指针 / 锁外返回 c_str() / 临时对象 c_str() 等 §9.3 反模式。
## 4. TLS 证书验证
**评级: F (完全未启用)**
```cpp
// L87-93 — HttpClientCtx 构造函数
ssl::context ssl_ctx{ssl::context::tlsv12_client};
HttpClientCtx() {
ssl_ctx.set_default_verify_paths(); // 设置了 CA 路径...
// ★ 但从未调用 set_verify_mode()
}
```
- **`set_verify_mode(ssl::verify_peer)` 未被调用**: 默认模式为 `ssl::verify_none`,接受**任何**证书(自签名、过期、域名不匹配)。
- **无 hostname 验证**: 即使启用 `verify_peer`OpenSSL 本身不做 hostname 匹配。需额外调用 `SSL_set1_host()` 或使用 `boost::asio::ssl::host_name_verification`。代码未做。
- **SNI 已设置** (L148): `SSL_set_tlsext_host_name` 调用存在,但仅影响 SNI 扩展,不含验证语义。
- **`set_default_verify_paths` 形同虚设**: CA 路径已配置,但因验证模式未开启,永不使用。
**影响**: 所有 AI API 调用 (含 `api_key` 的 Authorization header) 可被中间人任意拦截。CVSS 7.4 (High)。
## 5. 超时 (connect / read / write)
**评级: C (DNS 无超时可永久 hang)**
| 阶段 | 位置 | 超时 | 状态 |
|------|------|------|------|
| DNS 解析 | L142 `resolver.resolve()` | 无 | **缺失** — DNS 无响应则永久阻塞 |
| TCP 连接 | L154-156 | `connect_timeout` (默认 30s) | OK |
| SSL 握手 | L160-163 | `connect_timeout` (默认 30s) | OK |
| HTTP 写入 | L180-183 | `request_timeout` (默认 120s) | OK |
| HTTP 读头 | L188-191 | `request_timeout` (默认 120s) | OK |
| 流式读体 | L218-219 | `request_timeout` (默认 120s) | OK (每轮循环重设) |
| 非流式读体 | L241-242 | `request_timeout` (默认 120s) | OK (每轮循环重设) |
| SSL 关闭 | L250 `stream.shutdown()` | 无 | 低风险ec 参数,不抛异常) |
**关键缺失**: L142 的同步 `resolver.resolve()` 无法设置 socket 级超时stream 尚未创建)。需改用 `async_resolve` 或在独立线程中执行 resolver。
**可配置性**: L129-135 从 config service 读取 `http.connect_timeout` / `http.request_timeout`,有兜底默认值 (30s/120s)。
## 6. 错误码语义与重试
**评级: B**
- L261: `(result_code >= 200 && result_code < 300) ? 0 : -1` — 2xx 成功,其余返回 -1。
- 调用方可通过 `*status_code` 区分: -1 = 连接/超时/DNS 错误4xx/5xx = HTTP 错误码。语义正确。
- **无重试逻辑**: 无任何 retry。对低层 HTTP 客户端这是合理设计,重试应由上层 (AI 插件) 实现。
- 异常路径 (L251-254): `result_code = -1`, `result_body = e.what()` — 异常消息通过 response_body 传递给调用方,设计合理。
- L221 / L244: `if (ec) break;` — 流/非流 read_some 错误静默退出循环,错误原因不记录日志(全文零 `g_host->log` 调用)。
## 7. 并发安全
**评级: A**
- 每次 `do_post_stream` 调用创建新 `HttpClientCtx` (L125),内含独立 `io_context`。零共享状态。
- `g_host` (L33): `on_init` 写入一次,后续只读。`on_shutdown` 不置空L305-307 为空函数体)。
- `g_config_svc` (L34): 同上。
- 无全局 socket / stream / resolver 共享。每个请求自包含。
- 多线程并发调用 `http_post_json` 安全。
## 8. 资源清理
**评级: A**
- `HttpClientCtx` (L125): 栈上对象,内含 RAII `io_context` + `ssl::context`。析构自动清理。
- `ssl_stream` (L144), `resolver` (L141), `flat_buffer` (L145): 全部栈上 RAII。
- 早期退出路径: `goto done` (L150, L214) — 栈展开正确析构所有对象。
- 异常路径: `catch(std::exception&)` (L251) — 栈展开触发析构。
- L249 `cancel()` + L250 `stream.shutdown(ec)` — 正常路径优雅关闭。
**零泄漏路径**,包括异常和提前退出。无动态分配(除 `g_host->strdup` 的返回值由调用方管理)。
## 9. 缓冲区限制 / DoS
**评级: A**
- L187: `parser.body_limit(16 * 1024 * 1024)` — 16MB 响应体硬上限。防止恶意服务器返回无限大数据导致 OOM。
- `parse_headers_json` 无长度限制 (L40-80): 理论上调用方可传入超大 JSON但 headers_json 来源为同进程内其他插件(可信),非网络输入。低风险。
---
## TOP 3 严重问题
### 发现 1 — [严重] TLS 证书验证完全禁用 (L87-93)
**问题**: 从未调用 `ssl_ctx.set_verify_mode(ssl::verify_peer)`,默认接受任何证书。无 hostname 验证。所有 API key 和通信内容可被 MITM 拦截。
**修复方向**: 添加 `ssl_ctx.set_verify_mode(ssl::verify_peer)` + `ssl_ctx.set_verify_callback(ssl::host_name_verification(host))` 或在 handshake 后调用 `SSL_get_peer_certificate` + `X509_check_host`
### 发现 2 — [高] DNS 解析无超时 (L142)
**问题**: `resolver.resolve(host, port)` 同步调用,无法通过 `expires_after` 设置超时socket 尚不存在)。若 DNS 服务器不响应 → 线程永久阻塞 → 调用方 hang。
**修复方向**: 使用 `async_resolve` 并将 io_context 的 `run()``run_for()` 与超时结合,或在独立 future/promise 线程中执行 resolution 并使用 wait_for。
### 发现 3 — [中] 异常处理缺 catch(...) 兜底 (L251)
**问题**: 仅捕获 `std::exception&`,无 `catch(...)`。非标准异常 → 穿越 C ABI → `std::terminate()`。虽然 Boost.Beast/Asio 的已知异常均派生自 `std::exception`,但 §8.2 要求双级 catch 全覆盖。
**修复方向**: 在 `catch (std::exception&)` 之后追加 `catch (...) { result_code = -1; result_body = "unknown exception"; }`
---
## TLS 验证状态
**未启用**`ssl_ctx.set_verify_mode` 从未调用,默认 `verify_none` 接受一切证书。hostname 验证(`SSL_set1_host` / `boost::asio::ssl::host_name_verification`)也未实现。`set_default_verify_paths` 的调用形同虚设。
**影响评估**: 生产不可用。任何可访问网络路径的中间人可解密全部通信,包括 API 密钥、对话内容。在公共 WiFi 或不可信网络环境下,这是可直接利用的凭证泄露漏洞。
---
## 整体评级: C
| 维度 | 评级 |
|------|------|
| 跨 DLL 堆纪律 | A |
| 异常安全 ABI | B |
| 字符串返回生命周期 | A |
| TLS 证书验证 | F |
| 超时 | C |
| 错误码语义 | B |
| 并发安全 | A |
| 资源清理 | A |
| 缓冲区限制 | A |
| **综合** | **C** |
**总评**: RAII、堆纪律、字符串生命周期、并发安全均高质量。但 TLS 证书验证完全禁用 (F) 是致命安全缺陷DNS 无超时可无限 hang。两个问题 (TLS + DNS) 使该插件在任何生产环境中不可用。修复后预期可达 A 级。
---
## Findings Summary
| ID | Severity | Title |
|----|----------|-------|
| F-13.3-1 | CRITICAL | TLS 证书验证完全禁用:`set_verify_mode(ssl::verify_peer)` 未调用,默认 `verify_none` 接受任何证书,无 hostname 验证 (L87-93) |
| F-13.3-2 | HIGH | DNS 解析无超时:`resolver.resolve(host, port)` 同步调用socket 未创建无法设超时DNS 无响应则线程永久阻塞 (L142) |
| F-13.3-3 | MEDIUM | 异常处理缺 `catch(...)` 兜底:仅捕获 `std::exception&`,非标准异常 (SEH/自定义) 穿越 C ABI → `std::terminate()` (L251) |