AI Agent 沙箱与代码执行安全:隔离策略与实战方案

对比容器、WebAssembly、进程级隔离三种沙箱方案,结合实战代码讲解如何安全执行 Agent 生成的代码。

AgentList Team · 2026年4月21日
AI Agent沙箱代码执行Docker安全

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 bomb
  • cpu_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 的 evalexec__import__ctypessubprocess 等都能绕过静态分析。即使你检查了 import 语句,__builtins__ 的动态操作也能让你措手不及。沙箱和静态分析是互补的,不是替代关系。

误区二:"Docker 本身就是安全的" 默认配置的 Docker 容器并不安全。如果不设置 network_disabledread_onlymem_limitpids_limit,容器内的代码可以挖矿、扫描内网、甚至尝试容器逃逸。安全是配置出来的。

误区三:"超时就够了,不需要资源限制" timeout=30 防不了内存炸弹:一行 [0] * 10**10 在几秒内就能耗尽内存,触发 OOM Killer 影响宿主机上的其他服务。必须同时设置内存限制。

总结

  • 沙箱是 Agent 代码执行的基础前提,不是可选的额外安全层
  • Docker 容器是最通用的方案,但必须配置:禁网络、只读文件系统、内存限制、进程数限制
  • 进程级隔离适合低延迟场景,但安全性较弱,需配合模块黑名单和资源限制
  • WebAssembly 提供最强的安全保证,但语言支持有限
  • 超时限制防不了内存炸弹和 fork bomb——必须配合资源限制

本文由 AgentList 团队整理,更多 Agent 沙箱相关项目请浏览本站项目列表。