fix(aibox): close audit findings H1-H3/M1-M2 (command exec protection chain)
- 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.
This commit is contained in:
1 parent
ce142fe32f
commit
177a9ac9a6
6 files changed
+318
-224
No files matched your search
+12
-153
@@ -5,14 +5,15 @@ tools.command_tool - 命令行执行工具
|
||||
抽离自 aibox.py,使用抽象的 CommandExecutor 实现命令执行(流式 + 超时控制)。
|
||||
工具本身不持有 subprocess 逻辑,而是委托给 SubprocessCommandExecutor。
|
||||
|
||||
H1 修复(审计 2026-08-17):此前本类内置一套重复的 Popen 读取循环,
|
||||
完全绕过注入的 executor,导致 1MB 输出上限、cancel() 三层中断、
|
||||
超时机制三项保护同时失效。现改为纯委托,行为与 executor 完全一致。
|
||||
|
||||
依赖:common (ConfigManager, constants) + tools.base.Tool + subsystems.executors
|
||||
不反向依赖 aibox 任何符号。
|
||||
"""
|
||||
|
||||
import os
|
||||
import queue
|
||||
import subprocess
|
||||
import time
|
||||
from typing import Dict
|
||||
|
||||
from common import ConfigManager
|
||||
@@ -60,163 +61,21 @@ class CommandLineTool(Tool):
|
||||
}
|
||||
|
||||
def execute_streaming(self, output_queue: queue.Queue, **kwargs):
|
||||
"""流式执行命令,支持超时控制"""
|
||||
"""流式执行命令:纯委托给 executor(1MB 上限/超时/cancel 全部生效)"""
|
||||
command = kwargs.get("command")
|
||||
timeout = kwargs.get("timeout", 30)
|
||||
work_dir = kwargs.get("work_dir", ".")
|
||||
extra_env = kwargs.get("env", {})
|
||||
|
||||
if not command:
|
||||
output_queue.put(("line", "❌ 错误:缺少命令参数\n"))
|
||||
output_queue.put(("complete", "❌ 错误:缺少命令参数"))
|
||||
return
|
||||
|
||||
# 发送开始执行的消息
|
||||
output_queue.put(("line", f"\n🔧 执行命令: {command}\n"))
|
||||
output_queue.put(("line", f"⏱️ 超时设置: {timeout} 秒\n"))
|
||||
if work_dir != ".":
|
||||
output_queue.put(("line", f"📁 工作目录: {work_dir}\n"))
|
||||
output_queue.put(("line", "-" * 50 + "\n"))
|
||||
|
||||
# 准备环境变量
|
||||
env = os.environ.copy()
|
||||
if extra_env:
|
||||
env.update(extra_env)
|
||||
|
||||
try:
|
||||
# 使用 subprocess.Popen 实现流式输出,支持超时
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
env=env,
|
||||
cwd=os.path.expanduser(work_dir)
|
||||
)
|
||||
|
||||
start_time = time.time()
|
||||
output_lines = []
|
||||
timeout_occurred = False
|
||||
|
||||
# 实时读取输出
|
||||
while True:
|
||||
# 检查超时
|
||||
elapsed = time.time() - start_time
|
||||
if timeout > 0 and elapsed > timeout:
|
||||
process.terminate()
|
||||
timeout_occurred = True
|
||||
output_queue.put(("line", f"\n⏰ 命令执行超时({timeout}秒),已终止\n"))
|
||||
break
|
||||
|
||||
# 使用非阻塞方式读取一行
|
||||
import select
|
||||
import sys
|
||||
|
||||
# 检查是否有输出可读
|
||||
if sys.platform == 'win32':
|
||||
# Windows 下使用 readline 带超时
|
||||
try:
|
||||
import msvcrt
|
||||
if msvcrt.kbhit():
|
||||
pass
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
line = line.rstrip('\n')
|
||||
output_queue.put(("line", line + "\n"))
|
||||
output_lines.append(line)
|
||||
else:
|
||||
time.sleep(0.05)
|
||||
except:
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
line = line.rstrip('\n')
|
||||
output_queue.put(("line", line + "\n"))
|
||||
output_lines.append(line)
|
||||
else:
|
||||
# Unix 系统可以使用 select
|
||||
import select
|
||||
rlist, _, _ = select.select([process.stdout], [], [], 0.5)
|
||||
if rlist:
|
||||
line = process.stdout.readline()
|
||||
if not line and process.poll() is not None:
|
||||
break
|
||||
if line:
|
||||
line = line.rstrip('\n')
|
||||
output_queue.put(("line", line + "\n"))
|
||||
output_lines.append(line)
|
||||
else:
|
||||
# 检查进程是否结束
|
||||
if process.poll() is not None:
|
||||
# 读取剩余输出
|
||||
remaining = process.stdout.read()
|
||||
if remaining:
|
||||
remaining = remaining.rstrip('\n')
|
||||
if remaining:
|
||||
output_queue.put(("line", remaining + "\n"))
|
||||
output_lines.append(remaining)
|
||||
break
|
||||
|
||||
# 如果超时,确保进程被终止
|
||||
if timeout_occurred and process.poll() is None:
|
||||
process.kill()
|
||||
process.wait(timeout=2)
|
||||
|
||||
# 获取返回码
|
||||
return_code = process.returncode if process.returncode is not None else -1
|
||||
|
||||
output_queue.put(("line", "-" * 50 + "\n"))
|
||||
|
||||
if timeout_occurred:
|
||||
output_queue.put(("line", f"⏰ 命令执行超时({timeout}秒),返回码: {return_code}\n"))
|
||||
else:
|
||||
output_queue.put(("line", f"✅ 命令执行完成,返回码: {return_code}\n"))
|
||||
|
||||
# 准备完整结果
|
||||
full_output = "\n".join(output_lines)
|
||||
data = {
|
||||
"输出": full_output if full_output else "(无输出)",
|
||||
"返回码": return_code
|
||||
}
|
||||
|
||||
if timeout_occurred:
|
||||
result = self.format_result(False, f"命令执行超时({timeout}秒)", data)
|
||||
else:
|
||||
result = self.format_result(return_code == 0, "命令执行完成", data)
|
||||
|
||||
output_queue.put(("complete", result))
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
error_msg = f"❌ 命令执行超时({timeout}秒)"
|
||||
output_queue.put(("line", error_msg + "\n"))
|
||||
output_queue.put(("complete", error_msg))
|
||||
except FileNotFoundError as e:
|
||||
error_msg = f"❌ 命令执行错误: 命令未找到 - {str(e)}"
|
||||
output_queue.put(("line", error_msg + "\n"))
|
||||
output_queue.put(("complete", error_msg))
|
||||
except PermissionError as e:
|
||||
error_msg = f"❌ 命令执行错误: 权限不足 - {str(e)}"
|
||||
output_queue.put(("line", error_msg + "\n"))
|
||||
output_queue.put(("complete", error_msg))
|
||||
except Exception as e:
|
||||
error_msg = f"❌ 执行错误: {str(e)}"
|
||||
output_queue.put(("line", error_msg + "\n"))
|
||||
output_queue.put(("complete", error_msg))
|
||||
finally:
|
||||
# 确保进程被清理
|
||||
if 'process' in locals() and process and process.poll() is None:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=2)
|
||||
except:
|
||||
process.kill()
|
||||
self.executor.execute_streaming(
|
||||
command,
|
||||
output_queue,
|
||||
timeout=kwargs.get("timeout", 30),
|
||||
work_dir=kwargs.get("work_dir", "."),
|
||||
env=kwargs.get("env"),
|
||||
)
|
||||
|
||||
def execute(self, command: str, timeout: int = 30,
|
||||
work_dir: str = ".", env: Dict = None, **kwargs) -> str:
|
||||
|
||||
Reference in new issue
Block a user