AI Agent 沙箱与代码执行安全:隔离策略与实战方案
对比容器、WebAssembly、进程级隔离三种沙箱方案,结合实战代码讲解如何安全执行 Agent 生成的代码。
AI Agent 沙箱与代码执行安全:隔离策略与实战方案
当 Agent 需要运行自己生成的代码时,"只执行安全的代码"是个伪命题——LLM 无法可靠判断自己输出的代码是否安全。唯一可行的方案是:在隔离环境中执行所有代码,假设每一段代码都是恶意的。
Agent 为什么需要沙箱
很多团队的第一反应是"我们让 Agent 只生成安全的代码"。这条路走不通,原因有三:
- LLM 无法可靠判断代码安全性:一段看似无害的
os.listdir()可能被用于信息侦察,而eval()更是万恶之源 - 间接注入可以篡改代码生成:攻击者通过 prompt injection 让 Agent 生成恶意代码,而非直接注入恶意代码
- 即使是合法需求也可能出错:Agent 生成的代码可能包含无限循环、内存泄漏或意外的文件删除
因此,沙箱不是"额外的安全层",而是 Agent 代码执行能力的基础前提。
三种沙箱方案对比
| 方案 | 隔离强度 | 启动延迟 | 适用语言 | 实现复杂度 |
|---|---|---|---|---|
| Docker 容器 | 高 | 1-5s | 全部 | 中 |
| WebAssembly | 中高 | <100ms | 有限(Rust/C/Go/JS) | 高 |
| 进程级隔离 | 中 | <50ms | 全部 | 低 |
方案一:Docker 容器沙箱
最通用的方案。每个代码执行请求启动一个独立的 Docker 容器,执行完毕后销毁。
import docker
import tempfile
import os
class DockerSandbox:
def __init__(
self,
image: str = "python:3.12-slim",
memory_limit: str = "128m",
cpu_period: int = 100000,
cpu_quota: int = 50000, # 50% CPU
timeout: int = 30,
network_disabled: bool = True,
):
self.client = docker.from_env()
self.image = image
self.memory_limit = memory_limit
self.cpu_period = cpu_period
self.cpu_quota = cpu_quota
self.timeout = timeout
self.network_disabled = network_disabled
def execute(self, code: str, language: str = "python") -> dict:
# 将代码写入临时文件
ext_map = {"python": ".py", "javascript": ".js", "go": ".go"}
ext = ext_map.get(language, ".txt")
with tempfile.NamedTemporaryFile(mode="w", suffix=ext, delete=False) as f:
f.write(code)
host_path = f.name
try:
container = self.client.containers.run(
image=self.image,
command=f"python /code/main{ext}" if language == "python" else f"node /code/main{ext}",
volumes={host_path: {"bind": f"/code/main{ext}", "mode": "ro"}},
mem_limit=self.memory_limit,
memswap_limit=self.memory_limit, # 禁用 swap
cpu_period=self.cpu_period,
cpu_quota=self.cpu_quota,
network_disabled=self.network_disabled,
read_only=True, # 只读文件系统
tmpfs={"/tmp": "size=10m"}, # 小型可写 tmpfs
pids_limit=64, # 限制进程数,防止 fork bomb
detach=True,
remove=True,
)
result = container.wait(timeout=self.timeout)
stdout = container.logs(stdout=True, stderr=False).decode("utf-8", errors="replace")
stderr = container.logs(stdout=False, stderr=True).decode("utf-8", errors="replace")
return {
"exit_code": result.get("StatusCode", -1),
"stdout": stdout[:10000], # 截断输出
"stderr": stderr[:10000],
"timed_out": False,
}
except docker.errors.APIError as e:
if "timed out" in str(e).lower():
try:
container.kill()
except Exception:
pass
return {"exit_code": -1, "stdout": "", "stderr": "Execution timed out", "timed_out": True}
return {"exit_code": -1, "stdout": "", "stderr": str(e), "timed_out": False}
finally:
os.unlink(host_path)
关键安全配置:
network_disabled=True— 完全禁止网络访问,阻止数据外泄和远程代码下载read_only=True— 只读文件系统,防止写入恶意文件mem_limit+memswap_limit— 限制内存,防止内存炸弹pids_limit— 限制进程数,防止 fork bombcpu_quota— 限制 CPU 使用,防止无限循环占满资源
方案二:进程级隔离(快速执行场景)
对于需要极低延迟的场景(如在线代码助手),进程级隔离是更实际的选择。
import subprocess
import resource
import signal
class ProcessSandbox:
def __init__(self, timeout: int = 10, max_memory_mb: int = 64):
self.timeout = timeout
self.max_memory = max_memory_mb * 1024 * 1024 # bytes
def execute(self, code: str) -> dict:
import tempfile, os
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
# 注入安全限制
safe_code = self._inject_safety(code)
f.write(safe_code)
path = f.name
try:
proc = subprocess.Popen(
["python", "-S", path], # -S 跳过 site packages
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=self._set_limits,
env={"PATH": "/usr/bin:/bin"}, # 最小 PATH
)
try:
stdout, stderr = proc.communicate(timeout=self.timeout)
return {
"exit_code": proc.returncode,
"stdout": stdout.decode("utf-8", errors="replace")[:10000],
"stderr": stderr.decode("utf-8", errors="replace")[:10000],
"timed_out": False,
}
except subprocess.TimeoutExpired:
proc.kill()
return {"exit_code": -1, "stdout": "", "stderr": "Timeout", "timed_out": True}
finally:
os.unlink(path)
def _set_limits(self):
"""在子进程中设置资源限制"""
resource.setrlimit(resource.RLIMIT_AS, (self.max_memory, self.max_memory))
resource.setrlimit(resource.RLIMIT_CPU, (self.timeout, self.timeout))
resource.setrlimit(resource.RLIMIT_NOFILE, (10, 10)) # 限制打开文件数
def _inject_safety(self, code: str) -> str:
"""注入安全限制代码"""
blocked_imports = ["os", "subprocess", "socket", "http", "urllib", "requests", "shutil"]
restrictions = [
"import sys",
"# Block dangerous builtins",
"__builtins__ = {k: v for k, v in __builtins__.items() if k not in ['exec', 'eval', 'compile', 'open', 'input']}",
]
for mod in blocked_imports:
restrictions.append(
f"sys.modules['{mod}'] = None # blocked"
)
return "\n".join(restrictions) + "\n\n" + code
优势:启动延迟 <50ms,适合需要快速反馈的场景。
局限:进程级隔离的安全性低于容器。preexec_fn 资源限制在 Python 中并非完全可靠,且模块黑名单方式容易被绕过。
方案三:WebAssembly(浏览器 + 服务器)
WebAssembly 提供了真正的最小权限沙箱——WASM 模块默认无法访问文件系统、网络或系统调用。
# 服务端 WASM 执行示例(使用 wasmtime)
from wasmtime import Store, Module, Instance, WasiConfig
import tempfile, os
class WasmSandbox:
def __init__(self, wasm_bytes: bytes):
self.store = Store()
self.module = Module(self.store.engine, wasm_bytes)
def execute(self, input_data: str) -> str:
# 配置 WASI(WebAssembly System Interface)
wasi_config = WasiConfig()
wasi_config.stdin_data = input_data.encode("utf-8")
# 禁止所有文件系统和网络访问
wasi_config.preopen_dir(".", "/sandbox", readonly=True)
self.store.set_wasi(wasi_config)
instance = Instance(self.store, self.module)
# 调用 _start 函数(WASM 入口点)
start = instance.exports(self.store)["_start"]
start(self.store)
return self._read_stdout()
def _read_stdout(self) -> str:
# 从 WASI stdout 缓冲区读取输出
pass
优势:理论上最安全的沙箱方案——WASM 模块只能做你明确授权的事。
局限:语言支持有限(需要编译为 WASM),不适合需要完整标准库的 Python 代码执行。
如何选择
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| 执行任意 Python 代码 | Docker 容器 | 隔离最强,语言支持最全 |
| 在线代码助手(低延迟) | 进程级隔离 | 启动快,可接受较弱隔离 |
| 执行特定算法(无 I/O) | WebAssembly | 安全性最高,启动极快 |
| 前端 Agent 执行用户代码 | WebAssembly(浏览器) | 零服务器成本,天然隔离 |
| 需要文件系统读写 | Docker + tmpfs | 容器内提供临时可写空间 |
常见误区
误区一:"代码静态分析就够了,不需要沙箱"
Python 的 eval、exec、__import__、ctypes、subprocess 等都能绕过静态分析。即使你检查了 import 语句,__builtins__ 的动态操作也能让你措手不及。沙箱和静态分析是互补的,不是替代关系。
误区二:"Docker 本身就是安全的"
默认配置的 Docker 容器并不安全。如果不设置 network_disabled、read_only、mem_limit、pids_limit,容器内的代码可以挖矿、扫描内网、甚至尝试容器逃逸。安全是配置出来的。
误区三:"超时就够了,不需要资源限制"
timeout=30 防不了内存炸弹:一行 [0] * 10**10 在几秒内就能耗尽内存,触发 OOM Killer 影响宿主机上的其他服务。必须同时设置内存限制。
总结
- 沙箱是 Agent 代码执行的基础前提,不是可选的额外安全层
- Docker 容器是最通用的方案,但必须配置:禁网络、只读文件系统、内存限制、进程数限制
- 进程级隔离适合低延迟场景,但安全性较弱,需配合模块黑名单和资源限制
- WebAssembly 提供最强的安全保证,但语言支持有限
- 超时限制防不了内存炸弹和 fork bomb——必须配合资源限制
本文由 AgentList 团队整理,更多 Agent 沙箱相关项目请浏览本站项目列表。
本文涉及的项目
ZeroBox
580 ⭐轻量级跨平台进程沙箱工具,基于OpenAI Codex运行时构建,支持文件、网络和凭证控制,可安全隔离执行任意命令。
LLM Sandbox
1.1k ⭐轻量级且可移植的LLM沙箱运行时Python库,提供代码解释器功能,支持在隔离环境中安全执行AI Agent生成的代码。
Dify Sandbox
1.2k ⭐轻量级、快速且安全的代码执行环境,支持多种编程语言,为Dify平台提供沙箱化的代码运行能力。
WebContainer
4.6k ⭐在浏览器中运行Node.js运行时环境的开源引擎,支持在Web应用中实现完全沙箱化的开发环境,无需服务器端执行。
OpenSandbox
10.6k ⭐OpenSandbox 是阿里巴巴开源的安全、快速、可扩展的 AI Agent 沙箱运行时环境。