- H1: CommandLineTool.execute_streaming now purely delegates to injected executor; removes 150-line duplicate Popen loop that bypassed 1MB cap, cancel() and timeout protections - H2: thread the blocking readline into a daemon reader thread; main loop polls queue with 0.1s ticks so timeout/1MB checks fire even for silent commands (verified: 3s timeout on 15s sleep, was 15s+ before) - H3: stdin=DEVNULL on both Popen sites; stdin-waiting commands now hit EOF instead of deadlocking the tool thread forever - M1: heredoc delimiter uniquification in write_file guidance (content line equal to AIBOX_EOF no longer truncates into shell injection) - M2: 3600s total deadline on registry streaming forward loop Verification: PoC regression 5/5 PASS (timeout/1MB/cancel/delegation), full unit suite 264 passed. Audit report updated with Batch E results.
7.2 KiB
aibox 模块安全与质量审计报告
- 日期:2026-08-17
- 审计基线:HEAD
ce142fe(Batch A–D 落地后) - 审计范围:
aibox.py(~3150 行)+llm_client.py+tools/(7 文件)+subsystems/(6 文件)+common/ - 方法:逐文件代码审读 + PoC 动态验证(H2 已实证)
一、总体结论
架构设计(分层、参数化 SQL、密钥隔离、消息回滚)整体健康,但命令执行链存在一条系统性断裂:实际运行路径绕过了精心设计的执行器,导致 1MB 硬约束、超时、中断三项保护同时失效。
二、高危发现(3 项)
H1:run_command 实际路径绕过注入的 executor——三重保护同时失效
证据链:
aibox.py:1012将command_executor注入CommandLineTool- 但
tools/command_tool.py:86-219的execute_streaming自带一套 150 行的重复 Popen 实现,self.executor全程未使用(死注入) tools/registry.py:71-86检测到execute_streaming即走该路径——生产 100% 命中
后果:
| 保护 | 设计位置 | 实际状态 |
|---|---|---|
| 1MB 输出上限(硬约束) | executors.py:69 |
失效——command_tool 的 output_lines 无限累积 |
cancel() 中断 |
executors.py:76-86 只 kill _current_process |
失效——实际进程引用只在 command_tool 局部变量,三层中断的第三层对 run_command 不生效 |
| 超时/截断统一语义 | executors | 旁路(见 H2) |
附带问题:msvcrt.kbhit() 检查的是当前进程键盘而非子管道(command_tool.py:123-125 无意义代码);循环内重复 import select。
H2:超时机制对无输出命令完全失效(PoC 已证)
readline() 是阻塞调用,超时检查只在两次 readline 之间执行。无输出命令会让 readline 阻塞到进程自然退出:
PoC:timeout=3s,命令 powershell Start-Sleep 15(无 stdout 输出)
结果:readline blocked 15.2s (timeout was 3s)
VERDICT: timeout mechanism ineffective for silent commands: True
两处同病:command_tool.py:106-165 与 executors.py:97-113。--once 模式有 --once-timeout 兜底,WebSocket 模式无兜底。
H3:stdin=subprocess.PIPE 从不写入也不关闭——死锁放大器
executors.py:156、command_tool.py:93:等待 stdin 的命令(cat 无参、交互式 python、ssh)永不退出 → 与 H2 叠加:readline 永久阻塞 + 超时检测永不执行 + cancel 杀不到进程 = 工具线程永久挂起。应为 stdin=subprocess.DEVNULL。
三、中危发现(4 项)
| # | 问题 | 位置 | 说明 |
|---|---|---|---|
| M1 | heredoc 定界符注入 | basic_tools.py:179 |
content 含独立行 AIBOX_EOF 时 heredoc 提前终止,后续行成为 shell 命令执行。AI 写文件内容是不可信输入(可被网页内容/用户消息诱导)。修复:定界符唯一化(检测冲突则换 AIBOX_EOF_2 递增) |
| M2 | 工具流式转发循环可永久挂起 | registry.py:90-107 |
while thread.is_alive() or not empty() + get(0.1)——H3 场景下永不退出;join(5) 超时后返回但 daemon 线程继续向输出队列写(僵尸输出混入后续对话流) |
| M3 | web 工具无协议/目标校验 | basic_tools.py:315-338 |
url 未限制 scheme,curl file:///etc/shadow、内网地址均可达(SSRF)。本地单用户产品风险有限,建议白名单 http/https |
| M4 | shlex.quote POSIX 转义在 Windows 失效 | basic_tools.py:99-112 等全部指引命令 |
单引号在 cmd.exe 无效,Windows 下 head/sed/grep 指引命令执行异常。Linux 部署(树莓派)不受影响 |
四、低危发现(3 项)
| # | 问题 | 位置 |
|---|---|---|
| L1 | get_pending_memos 的 LIMIT {limit} f-string 拼接 |
db.py:417——当前调用链 limit 均为内部 int,无外部直达;防御性参数化即可 |
| L2 | check_path_safety 仅查大小,无路径边界 |
base.py:96-110——by design(AI 可全盘读写,本地单用户),已确认设计意图 |
| L3 | executors.execute() finally kill 后未 wait() |
executors.py:205-207——Windows 句柄短暂滞留 |
做得好的:SQL 全面参数化(LIKE 通配符正确转义 db.py:200-209)、API key 零日志泄漏、WAL 已启用、消息回滚/tool 配对修复、_msg_lock/_process_lock 使用正确、工具实例按连接隔离。
五、架构合规矩阵
| 硬约束 | 状态 |
|---|---|
| WebSocket :8765 | ✅ |
依赖单向 common←subsystems←tools←aibox |
✅ |
| Worker 仅 7 执行工具 | ✅ |
| 工具输出 ≤1MB + 超时 + 中断 | ❌ H1/H2/H3 三项全破 |
| prompt 三级排序加载 / sqlite 线程安全 | ✅ |
六、修复建议(一批搞定 H1/H2/H3,约 ±120 行)
- 删除 command_tool.py 的重复 Popen 实现,
execute_streaming改为一行委托self.executor.execute_streaming(output_queue, ...)→ 1MB、cancel、统一语义全部自动恢复 - executors.py 读取循环改线程化:后台 reader 线程做阻塞
readline入队列,主循环带超时轮询队列——跨平台解决 H2 stdin=subprocess.DEVNULL(H3)- M1 定界符唯一化 + M2 转发循环加总超时,顺手可修
七、修复落地(Batch E,2026-08-17)
五项修复(H1/H2/H3/M1/M2)已全部实施并通过验证:
| # | 修复 | 位置 | 状态 |
|---|---|---|---|
| H1 | CommandLineTool.execute_streaming 改为纯委托 executor.execute_streaming,删除 150 行重复 Popen(-165 行) |
tools/command_tool.py |
✅ |
| H2 | _read_with_limit 线程化:后台 reader 线程阻塞 readline 入队,主循环 0.1s 轮询 + 超时/1MB 检查;shell 已死但 reader 无 EOF 时 1s 宽限排空(Windows shell=True 孙进程持管道场景) |
subsystems/executors.py |
✅ |
| H3 | 两处 Popen 均改 stdin=subprocess.DEVNULL |
subsystems/executors.py |
✅ |
| M1 | heredoc 定界符唯一化:content 含独立行与 AIBOX_EOF 相同时递增 _X 后缀直到唯一 |
tools/basic_tools.py |
✅ |
| M2 | registry 转发循环加 3600s 总超时兜底,超时后放弃等待并返回错误信息 | tools/registry.py |
✅ |
PoC 回归验证(aibox/tests/_poc_audit_regression.py,5/5 PASS)
[PASS] PoC-1 无输出命令超时(H2): 3.0s 返回(修复前阻塞15s+),结果含超时: True
[PASS] PoC-2 stdin等待命令不挂起(H3): 0.2s 返回(修复前永久挂起)
[PASS] PoC-3 1MB输出截断: 0.2s,结果 1.00MB,含截断标记: True
[PASS] PoC-4 cancel中断: cancel 后 3.0s 结束(修复前杀不到进程需等满120s)
[PASS] PoC-5 工具层1MB贯通(H1): 0.2s,工具层结果 1.00MB(修复前无限累积)
PoC-2 说明:stdin=DEVNULL 下等待输入的命令立即收到 EOF 快速退出(0.2s),优于按超时返回;判定标准为"不永久挂起"。
单元测试
aibox 全量单测 264 passed(零回归)。
遗留项(本轮未处理)
- M3(web 工具 scheme/目标校验)、M4(Windows 下 shlex.quote 转义失效)、L1/L3(防御性加固)——低优先级,待后续批次
- 架构合规矩阵"工具输出 ≤1MB + 超时 + 中断"一项修复后为 ✅