Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Update audit tracker, roadmap, architecture docs, add admin-v2 Roles page + Billing tests, sync CLAUDE.md, Cargo.toml, docker-compose.yml, add deep-research / frontend-design / chart-visualization skills Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
330 lines
14 KiB
Markdown
330 lines
14 KiB
Markdown
# ZCLAW 深度二次审计补充报告
|
||
|
||
> **审计日期**: 2026-04-02
|
||
> **基线**: V11 全面审计报告(22 项发现)
|
||
> **方法**: 5 维并行深度审计
|
||
> **新增发现**: 22 项(2 P0 + 9 P1 + 10 P2 + 3 P3 + 1 确认项)
|
||
|
||
---
|
||
|
||
## 1. 审计维度
|
||
|
||
| # | 维度 | 发现数 |
|
||
|---|------|--------|
|
||
| 1 | Rust crate 交叉依赖 + 编译状态 | 5 |
|
||
| 2 | 前端 Store ↔ Tauri 命令参数签名全量比对 | 5 |
|
||
| 3 | Sprint 1-4 修复代码质量验证 | 1 |
|
||
| 4 | 安全/竞态/资源泄漏扫描 | 9 |
|
||
| 5 | 测试覆盖缺口 + admin-v2 API 对齐 | 4 |
|
||
|
||
---
|
||
|
||
## 2. 新增 P0 发现(2 项)
|
||
|
||
### SEC2-P0-01: skill_execute 反序列化崩溃
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | 前端传递空 `context: {}`,但 Rust `SkillContext` 要求 `agent_id: String` 和 `session_id: String`(非 Option)。空 JSON 无法反序列化。 |
|
||
| **前端** | `desktop/src/lib/kernel-skills.ts:110-114` |
|
||
| **Rust** | `desktop/src-tauri/src/kernel_commands/skill.rs:290-296` |
|
||
| **触发条件** | 用户在前端执行任何 Skill |
|
||
| **影响** | 运行时 serde 反序列化失败,Skill 执行必然报错 |
|
||
|
||
### SEC2-P0-02: TaskTool::default() 潜在 panic
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `TaskTool` 实现了 `Default` trait,但 `default()` 调用 `unimplemented!()` |
|
||
| **文件** | `crates/zclaw-runtime/src/tool/builtin/task.rs:59` |
|
||
| **触发条件** | 任何泛型约束 `T: Default` 触发 `TaskTool::default()` 时 |
|
||
| **影响** | 运行时 panic(虽然正常路径使用 `TaskTool::new()` 规避) |
|
||
| **建议** | 移除 `impl Default for TaskTool`,或提供合理的默认值 |
|
||
|
||
---
|
||
|
||
## 3. 新增 P1 发现(9 项)
|
||
|
||
### SEC2-P1-01: FactStore trait 零实现
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `FactStore` trait 在 `zclaw-memory/src/fact.rs:141` 定义,全 workspace 无任何 `impl FactStore for T`。`MemoryStore` 有同名方法但未实现 trait。`dyn FactStore` 模式完全不可用。 |
|
||
| **文件** | `crates/zclaw-memory/src/fact.rs` |
|
||
| **影响** | 架构层面功能缺口——trait 接口已定义但无法使用 |
|
||
|
||
### SEC2-P1-02: agent-templates API 路径缺少 /api/v1 前缀
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | 前端 saas-client.ts 中 agent-templates 路径缺少 `/api/v1` 前缀 |
|
||
| **前端** | `desktop/src/lib/saas-client.ts:376,384`:`'/agent-templates/available'`、`'/agent-templates/${id}/full'` |
|
||
| **后端** | 路由注册在 `/api/v1/agent-templates/...` |
|
||
| **影响** | 请求发送到错误 URL,必然返回 404 |
|
||
|
||
### SEC2-P1-03: hand-execution-complete 事件无前端监听
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | Rust 在 `hand.rs:279` 和 `approval.rs:130` emit `hand-execution-complete`,但前端无任何 `listen()` 调用 |
|
||
| **影响** | Hand 异步执行完成后,前端无法收到通知,用户不知道执行结果 |
|
||
|
||
### SEC2-P1-04: InMemoryStorage RwLock unwrap() 级联 panic 风险
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `InMemoryStorage` 中 6 处 `std::sync::RwLock` 的 `.unwrap()`。若锁被 poisoned(某线程 panic),后续所有调用级联崩溃 |
|
||
| **文件** | `crates/zclaw-growth/src/viking_adapter.rs:137,143,148,190,202,208,214` |
|
||
| **风险** | 低概率但后果严重 |
|
||
|
||
### SEC2-P1-05: HandRun 持久化错误静默忽略(3 处)
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `let _ = memory.save_hand_run(&run).await` 等 3 处 HandRun 持久化错误被忽略 |
|
||
| **文件** | `crates/zclaw-kernel/src/kernel/approvals.rs:88,91,120` |
|
||
| **影响** | DB 不可用时 run 状态丢失,UI 无法显示执行结果 |
|
||
| **建议** | 至少 log warning |
|
||
|
||
### SEC2-P1-06: FTS 索引更新失败静默忽略(3 处)
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | 全文搜索索引 DELETE/INSERT 操作失败被 `let _ =` 忽略 |
|
||
| **文件** | `crates/zclaw-growth/src/storage/sqlite.rs:384,390,605` |
|
||
| **影响** | 搜索结果不一致(stale index) |
|
||
|
||
### SEC2-P1-07: Worker dispatch 失败静默忽略(4 处)
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `let _ = state.worker_dispatcher.dispatch(...)` 4 处忽略 |
|
||
| **文件** | `crates/zclaw-saas/src/knowledge/handlers.rs:220,262,331,539` |
|
||
| **影响** | embedding 生成等后台任务可能静默丢失 |
|
||
|
||
### SEC2-P1-08: Desktop 前端零测试
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `desktop/src/` 中零个 `.test.ts`/`.test.tsx` 文件,包括 chatStore、agentStore 等核心 store |
|
||
| **影响** | 前端任何回归只能通过手工测试发现 |
|
||
|
||
### SEC2-P1-09: record_key_usage 错误忽略导致计费数据丢失风险
|
||
|
||
| 维度 | 详情 |
|
||
|------|------|
|
||
| **问题** | `let _ = record_key_usage(...)` 忽略写入错误 |
|
||
| **文件** | `crates/zclaw-saas/src/relay/service.rs:376` |
|
||
| **影响** | 可能导致计费数据丢失 |
|
||
|
||
---
|
||
|
||
## 4. 新增 P2 发现(10 项)
|
||
|
||
| ID | 问题 | 文件 |
|
||
|----|------|------|
|
||
| SEC2-P2-01 | `hmac`/`sha1` 在 zclaw-hands Cargo.toml 中声明但代码零引用 | `crates/zclaw-hands/Cargo.toml` |
|
||
| SEC2-P2-02 | `serde_yaml` 版本不一致:desktop 用 0.9,pipeline 用 2(不同 package) | 各 `Cargo.toml` |
|
||
| SEC2-P2-03 | `sqlx-postgres v0.7.4` 使用了未来 Rust 版本将拒绝的语法 | `Cargo.lock` |
|
||
| SEC2-P2-04 | `generate_embedding.rs:107` embedding 生成被注释掉(`// TODO`) | `crates/zclaw-saas/src/workers/generate_embedding.rs` |
|
||
| SEC2-P2-05 | ~10 处 `tokio::spawn` 返回 JoinHandle 未绑定,无法优雅停止 | kernel + saas 多处 |
|
||
| SEC2-P2-06 | Telemetry 审计日志批量 INSERT 绑定可能与 SQL 模板不匹配 | `crates/zclaw-saas/src/telemetry/service.rs:205-213` |
|
||
| SEC2-P2-07 | Scheduler 串行执行 trigger,长 hand 阻塞后续调度 | `crates/zclaw-kernel/src/scheduler.rs:117-153` |
|
||
| SEC2-P2-08 | `format!("FROM {}", table)` 模式虽当前安全(硬编码),但违反防御原则 | `crates/zclaw-saas/src/db.rs:874,880` |
|
||
| SEC2-P2-09 | `hand_run_status` 前端传递多余 `handName` 参数(Rust 只接收 `run_id`) | `desktop/src/lib/kernel-hands.ts:95` |
|
||
| SEC2-P2-10 | `kernel_apply_saas_config` TOML 写入不支持多行值 | `desktop/src-tauri/src/kernel_commands/lifecycle.rs` |
|
||
|
||
---
|
||
|
||
## 5. 新增 P3 发现(3 项)
|
||
|
||
| ID | 问题 | 文件 |
|
||
|----|------|------|
|
||
| SEC2-P3-01 | A2A Router 4 个 RwLock 获取顺序未文档化 | `crates/zclaw-protocols/src/a2a.rs:239-245` |
|
||
| SEC2-P3-02 | Admin-v2 Role 类型:后端 `is_system` 字段前端未映射,前端 `account_count` 后端无字段 | `admin-v2/src/types/index.ts` vs `role/types.rs` |
|
||
| SEC2-P3-03 | Admin-v2 Billing/Knowledge/Roles 三个页面缺测试(Billing、Knowledge 页面 + Roles 新页面) | `admin-v2/tests/` |
|
||
|
||
---
|
||
|
||
## 6. 确认项(审计验证结果)
|
||
|
||
### 6.1 编译状态
|
||
|
||
- `cargo check --workspace` **编译成功**(33.60s)
|
||
- 2 个 warning:`RegisterDeviceRequest` 可见性(P2)、`sqlx-postgres` 未来兼容性(P1)
|
||
|
||
### 6.2 Sprint 1-4 修复验证
|
||
|
||
| 修复 | 验证结果 |
|
||
|------|---------|
|
||
| trigger_update 参数扁平化 | **通过** — 前后端参数完全匹配 |
|
||
| SaaS config sync 传播 | **通过** — kernel_apply_saas_config 正确桥接 |
|
||
| Deprecated 代码清理 | **通过** — 无残留引用 |
|
||
| Admin Roles 页面 | **通过** — API 路径与 SaaS 路由匹配 |
|
||
| ToolDefinition 去重 | **通过** — 使用 pub use |
|
||
| Knowledge handler 修复 | **通过** — 连接到 service 层 |
|
||
| Task 结果持久化 | **通过** — migration + 读写一致 |
|
||
|
||
### 6.3 Admin-v2 ↔ SaaS API 对齐
|
||
|
||
**结论:100% 对齐。** 所有 admin-v2 前端调用的 API 端点在后端都有对应路由注册,包括:
|
||
- 9 个 knowledge 路径(categories/items/analytics/search/recommend/import/versions/rollback/batch)
|
||
- 5 个 role 路径 + 5 个 permission-template 路径
|
||
- 6 个 billing 路径
|
||
- 2 个 telemetry 路径
|
||
|
||
### 6.4 Feature Gate 一致性
|
||
|
||
传播链完全正确:
|
||
```
|
||
desktop --multi-agent--> zclaw-kernel --multi-agent--> zclaw-protocols --a2a--> [a2a module]
|
||
zclaw-skills --wasm--> [wasmtime, wasmtime-wasi]
|
||
```
|
||
|
||
### 6.5 认证覆盖
|
||
|
||
SaaS 后端三层路由分离无遗漏:公共路由 + 受保护路由 + Relay 独立中间件链。
|
||
|
||
---
|
||
|
||
## 7. 测试覆盖统计
|
||
|
||
### 7.1 Rust 测试分布
|
||
|
||
| Crate | 测试数 | 关键缺口 |
|
||
|-------|--------|----------|
|
||
| zclaw-hands | 155 | - |
|
||
| zclaw-saas | 92 | 集中在工具函数,handler/scheduler 0 测试 |
|
||
| zclaw-growth | 75 | - |
|
||
| zclaw-pipeline | 59 | - |
|
||
| zclaw-types | 57 | - |
|
||
| zclaw-kernel | 52 | capabilities/registry/trigger_manager 0 测试 |
|
||
| zclaw-runtime | 42 | - |
|
||
| zclaw-memory | 25 | - |
|
||
| zclaw-skills | 22 | - |
|
||
| zclaw-protocols | 5 | 极低 |
|
||
| **总计** | **584** | |
|
||
|
||
### 7.2 零测试关键模块
|
||
|
||
| 模块 | 公开函数数 | 严重度 |
|
||
|------|-----------|--------|
|
||
| kernel_commands/ (41 Tauri 命令) | 41 | P0 |
|
||
| browser/commands.rs (23 命令) | 23 | P0 |
|
||
| SaaS scheduler.rs | 3 公开函数 | P1 |
|
||
| SaaS knowledge/handlers.rs (561 行) | ~15 | P1 |
|
||
| SaaS relay/service.rs | ~20 | P1 |
|
||
| SaaS billing/ | ~10 | P1 |
|
||
| Desktop frontend | 全部 | P1 |
|
||
|
||
### 7.3 Admin-v2 测试
|
||
|
||
- **测试框架**: Vitest + jsdom + MSW
|
||
- **测试用例**: 322 个
|
||
- **覆盖页面**: 10/13(缺失 Billing、Knowledge、Roles)
|
||
- **覆盖 service**: 1/17(仅 request.ts)
|
||
|
||
---
|
||
|
||
## 8. 积极发现
|
||
|
||
1. **SQL 参数化规范**:SaaS 层绝大多数 SQL 使用 `sqlx::query(...).bind(...)` 参数化
|
||
2. **密钥保护**:JWT secret 使用 `secrecy::SecretString`,API key 日志仅输出 ID
|
||
3. **DashMap 死锁已规避**:所有 DashMap RefMut 在 `.await` 前释放
|
||
4. **SSE 背压设计**:有界 channel + 信号量限制 + CancellationToken
|
||
5. **密码版本控制**:password_version 确保 JWT 失效
|
||
6. **MCP Transport**:实现 Drop trait 清理子进程
|
||
7. **DB 连接池监控**:30s 周期日志 + 80% 告警
|
||
8. **Admin-v2 API 对齐**:所有前端调用都有后端路由对应
|
||
|
||
---
|
||
|
||
## 9. 综合风险矩阵(V11 + 二次审计合并)
|
||
|
||
| 严重度 | V11 原始 | 二次审计新增 | 合计 |
|
||
|--------|----------|-------------|------|
|
||
| P0 | 1 (误报) | **2** | 2 |
|
||
| P1 | 3 | **9** | 12 |
|
||
| P2 | 6 | **10** | 16 |
|
||
| P3 | 8 | **3** | 11 |
|
||
| P4 | 5 | 0 | 5 |
|
||
| **总计** | **23** | **24** | **46** |
|
||
|
||
---
|
||
|
||
## 10. 支付安全深度审计 V3(2026-04-02)
|
||
|
||
> **审计范围**: `crates/zclaw-saas/src/billing/` 全部文件(payment.rs, service.rs, handlers.rs, types.rs, mod.rs)
|
||
> **审计方法**: 3 维并行(Security Engineer + Code Reviewer + Senior Developer)
|
||
> **基线**: V2 审计后的 25 项修复全部落地
|
||
|
||
### 10.1 审计结论
|
||
|
||
| 指标 | 结果 |
|
||
|------|------|
|
||
| **CRITICAL** | **0** |
|
||
| **HIGH** | **0** |
|
||
| **MEDIUM** | **1**(已修复) |
|
||
| **LOW** | **3**(pre-existing / cosmetic) |
|
||
|
||
### 10.2 已修复项(本次修复)
|
||
|
||
| ID | 严重度 | 问题 | 修复 |
|
||
|----|--------|------|------|
|
||
| PAY-FIX-01 | MEDIUM | `truncate_str` 使用 `s.len()`(字节数)判断是否截断,但 `chars().take()` 按字符截断。中文等多字节字符串在 200 字节内但超过 200 字符时截断错误 | 改为先 `chars().collect::<Vec<char>>()` 再按 `.len()` 判断字符数 |
|
||
|
||
### 10.3 已验证通过的关键安全措施(25 项)
|
||
|
||
#### 事务与竞态保护(6 项)
|
||
- [x] `create_payment` 在事务中创建 invoice + payment(原子性)
|
||
- [x] `handle_payment_callback` 使用 `SELECT ... FOR UPDATE` 锁定行(防 TOCTOU)
|
||
- [x] `get_or_create_usage` 使用 `INSERT ON CONFLICT DO NOTHING`(防重复创建)
|
||
- [x] 金额交叉验证:生产环境 `callback_amount` 为 None 时拒绝
|
||
- [x] 幂等保护:已处理的 payment(status != pending)直接返回
|
||
- [x] 事务失败时正确 rollback
|
||
|
||
#### 支付回调安全(6 项)
|
||
- [x] Alipay 验签:生产环境强制 RSA2 验签,缺公钥拒绝
|
||
- [x] WeChat 解密:AES-256-GCM + nonce 长度校验(12 字节)
|
||
- [x] trade_no 缺失时返回错误而非静默忽略
|
||
- [x] 日志安全:`sanitize_log` 只保留字母数字和 `-` `_`
|
||
- [x] 通用错误对外返回(不泄露内部细节)
|
||
- [x] 回调路由正确分离到 public_routes(无需认证)
|
||
|
||
#### 密钥保护(4 项)
|
||
- [x] `PaymentConfig` 自定义 Debug impl,敏感字段显示 `***REDACTED***`
|
||
- [x] `alipay_private_key`、`alipay_public_key`、`wechat_api_v3_key` 标记 `skip_serializing`
|
||
- [x] JWT 密钥使用 `secrecy::SecretString`
|
||
- [x] API key 日志仅输出 ID 不输出值
|
||
|
||
#### 货币精度(2 项)
|
||
- [x] 分→元转换使用整数运算(`amount_cents / 100` + `amount_cents % 100`)
|
||
- [x] WeChat 金额保持整数分(无浮点转换)
|
||
|
||
#### HTML 安全(2 项)
|
||
- [x] Mock 支付页面使用 `html_escape()` 转义用户输入
|
||
- [x] Mock 确认页面的 `msg` 变量由服务端控制(安全)
|
||
|
||
#### SQL 安全(3 项)
|
||
- [x] 所有 SQL 使用参数化查询(`sqlx::query(...).bind(...)`)
|
||
- [x] `increment_dimension` 使用白名单分支(非动态列名)
|
||
- [x] `failure_reason` 截断到 200 字符
|
||
|
||
#### 整体架构(2 项)
|
||
- [x] 路由分离:`routes()` 需认证,`callback_routes()` 公开
|
||
- [x] 批量递增 `increment_dimension_by` 替代循环查询
|
||
|
||
### 10.4 剩余 LOW 项(不影响安全,可后续优化)
|
||
|
||
| ID | 说明 | 建议 |
|
||
|----|------|------|
|
||
| PAY-LOW-01 | `reqwest::Client::new()` 每次 WeChat 支付创建新实例 | 改为 AppState 中共享 Client |
|
||
| PAY-LOW-02 | `BootstrapScreen` 缺少 dark mode 变体 | 添加 `dark:bg-gray-900` |
|
||
| PAY-LOW-03 | `aside.w-64.sidebar-open` class 定义但无 JS 切换逻辑 | 添加汉堡菜单或移除 |
|
||
|
||
### 10.5 审计签名
|
||
|
||
- **Security Engineer**: 0 CRITICAL, 0 HIGH — 支付链路安全措施完备
|
||
- **Code Reviewer**: 25 项修复全部正确实现,无回归
|
||
- **Senior Developer**: 1 MEDIUM 已修复(truncate_str),代码质量良好
|