欢迎光临
我们一直在努力

生产沙箱逃逸防御:如何安全执行大模型生成的 Python 代码与 Shell 命令

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

封面信息图

上周四傍晚,安全组的同事突然端着笔记本站到了我工位旁,脸色严峻地调出了一份抓包记录。那是下午三点多红蓝对抗演练时截获的一段内网流量:公司新上线的报表生成 Agent 在解析一个由测试人员上传的恶意 CSV 文件时,被提示词越狱诱导,大模型直接生成了一段混淆的 Python 脚本。脚本里明晃晃地写着读取 /proc/1/environ 提取云厂商 AccessKey,并尝试通过反弹 Shell 扫描内网 10.0.0.0/16 网段。

虽然网络防火墙拦截了外部出口流量,但看着那条直奔 Kubernetes 集群元数据接口的 HTTP 请求,我后背全是冷汗。很多研发团队以为大模型生成的代码就是“普通的后台脚本”,随手用 os/exec 或者起一个裸 Docker 容器 docker run 就算完事。但面对拥有自主规划能力的 Agent,大模型生成的每一行代码在本质上都属于“不可信的远程任意代码执行(RCE)”。一旦沙箱存在一丝缝隙,容器逃逸、主机凭据泄露、内网横向渗透或者简单的 Fork 炸弹耗尽主机 PID,分分钟就能让整套集群趴下。

大模型代码执行的四大致命风险

要防范沙箱逃逸,必须先摸清大语言模型生成代码在生产环境下的攻击面:

  • 环境与凭据盗取:容器内的 /proc 文件系统、未清理的环境变量(如 AWS_SECRET_ACCESS_KEY、KUBERNETES_SERVICE_HOST)、以及挂载的 ServiceAccount Token,是脚本尝试刺探的第一目标。
  • 网络侧横向移动:即使脚本无法提权,只要它具备基础的网络能力,就能利用本地 socket 扫描内网未授权的 Redis、ClickHouse 或内部微服务,充当内网跳板。
  • 宿主机文件破坏与逃逸:如果容器以 root 用户运行且未清空 Linux Capabilities,攻击者可以通过挂载伪设备文件、利用内核 CVE 漏洞甚至直接篡改挂载的宿主机目录完成逃逸。
  • 资源耗尽型拒绝服务(DoS):大模型偶尔会生成死循环、无上限的递归、或者恶意的 Fork 炸弹(如 while True: os.fork()),几秒钟内把宿主机系统的 PID 计数器吃满,导致整台宿主机无法创建任何新进程,必须硬重启。
  • 纵深防御体系:只读 Rootfs、Drop ALL 与纯净隔离

    为了彻底封死这些漏洞,我们把原先简陋的容器执行方案全部废弃,基于 Linux 内核底层机制重构了一套轻量级代码沙箱执行器。这套架构建立在五道硬性防线之上:

    • 第一道防线:只读根文件系统(Read-Only Rootfs)。整个沙箱的根镜像以只读模式挂载,禁止修改系统目录下的任何二进制文件。仅挂载一个大小严格受限(比如 64MB)的独立内存文件系统(tmpfs)作为当前工作区,执行完毕立即彻底卸载销毁。
    • 第二道防线:剥离所有 Linux Capabilities(Drop ALL)。强制使用普通用户(UID 10001)运行,并且在进程启动时直接放弃全部 41 种内核 Capabilities,彻底杜绝 CAP_SYS_ADMIN、CAP_NET_RAW 等特权,封死提权可能。
    • 第三道防线:网络物理切断(No-Network Namespace)。沙箱容器独享一个没有任何网络接口的 Network Namespace,除了回环设备(lo)以外不配置任何虚拟网卡,哪怕大模型写出天花乱坠的 socket 扫描代码,也会在握手阶段被内核直接返回 Network unreachable。
    • 第四道防线:cgroups v2 严格配额。为单次执行强制绑定独立 cgroup:CPU 限制为 0.5 核,内存限制为 256MB(触发立即 OOMKilled),PIDs 限制为 32(防止 Fork 炸弹打爆系统)。
    • 第五道防线:内核调用白名单(SECCOMP 过滤)。禁用 ptrace、mount、bpf、sys_chroot 等高危系统调用。

    Go 1.27.1 实现的沙箱进程调度执行器

    下面是我们用 Go 1.27.1 编写的核心沙箱调度执行引擎。它利用 Go 底层系统调用参数,结合 context 超时控制,实现了确定性的沙箱生命周期管理。

    package sandbox

    import (
    "context"
    "errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "syscall"
    "time"
    )

    var (
    ErrSandboxTimeout = errors.New("sandbox: execution timed out")
    ErrResourceExceed = errors.New("sandbox: resource quota exceeded")
    )

    // ExecutionConfig 沙箱执行参数约束
    type ExecutionConfig struct {
    Timeout time.Duration
    MemoryLimitMB int64
    MaxOutputSize int64
    ScriptContent string
    }

    // ExecutionResult 执行结果与审计信息
    type ExecutionResult struct {
    ExitCode int
    Stdout string
    Stderr string
    Duration time.Duration
    }

    // SandboxExecutor 沙箱调度执行器
    type SandboxExecutor struct {
    baseSandboxImage string
    }

    func NewSandboxExecutor(baseImage string) *SandboxExecutor {
    return &SandboxExecutor{
    baseSandboxImage: baseImage,
    }
    }

    // ExecutePythonSafe 在高强度隔离环境下执行未知 Python 脚本
    func (s *SandboxExecutor) ExecutePythonSafe(ctx context.Context, cfg ExecutionConfig) (*ExecutionResult, error) {
    // 1. 创建临时的只读工作目录挂载点
    scratchDir, err := os.MkdirTemp("", "mcp_sandbox_*")
    if err != nil {
    return nil, fmt.Errorf("failed to create sandbox scratch dir: %w", err)
    }
    defer os.RemoveAll(scratchDir)

    scriptPath := filepath.Join(scratchDir, "main.py")
    if err := os.WriteFile(scriptPath, []byte(cfg.ScriptContent), 0400); err != nil {
    return nil, fmt.Errorf("failed to write script: %w", err)
    }

    // 2. 派生带强制超时控制的 Context
    execCtx, cancel := context.WithTimeout(ctx, cfg.Timeout)
    defer cancel()

    // 3. 构造极其苛刻的底层容器运行参数
    // 核心:–read-only –network none –cap-drop ALL –pids-limit 32 –memory 256m
    cmdArgs := []string{
    "run", "–rm",
    "–name", fmt.Sprintf("sbx-%d", time.Now().UnixNano()),
    "–network", "none",
    "–read-only",
    "–cap-drop", "ALL",
    "–security-opt", "no-new-privileges",
    "–pids-limit", "32",
    "–cpus", "0.5",
    "–memory", fmt.Sprintf("%dm", cfg.MemoryLimitMB),
    "–tmpfs", "/tmp:rw,noexec,nosuid,size=32m",
    "-v", fmt.Sprintf("%s:/app:ro", scratchDir),
    "-w", "/app",
    "–user", "10001:10001",
    s.baseSandboxImage,
    "python3", "main.py",
    }

    cmd := exec.CommandContext(execCtx, "podman", cmdArgs…)

    // 4. 利用 syscall 特性剥离进程组,防止主进程退出后遗留僵尸子进程
    cmd.SysProcAttr = &syscall.SysProcAttr{
    Setpgid: true,
    }

    startTime := time.Now()
    output, err := cmd.CombinedOutput()
    duration := time.Since(startTime)

    if execCtx.Err() == context.DeadlineExceeded {
    return nil, ErrSandboxTimeout
    }

    exitCode := 0
    if err != nil {
    var exitErr *exec.ExitError
    if errors.As(err, &exitErr) {
    exitCode = exitErr.ExitCode()
    } else {
    return nil, fmt.Errorf("sandbox system failure: %w", err)
    }
    }

    // 截断输出,防止大模型脚本狂打日志吃满网关内存
    trimmedOutput := string(output)
    if int64(len(trimmedOutput)) > cfg.MaxOutputSize {
    trimmedOutput = trimmedOutput[:cfg.MaxOutputSize] + "\\n[OUTPUT TRUNCATED BY SANDBOX]"
    }

    return &ExecutionResult{
    ExitCode: exitCode,
    Stdout: trimmedOutput,
    Duration: duration,
    }, nil
    }

    踩坑复盘:临时文件写入与 Python 编译缓存

    在落地这套极致防卫沙箱时,我们踩过两个非常隐蔽的工程坑:

    第一个坑是 Python 字节码写入报错。因为我们把工作目录挂载为只读(:ro),而 Python 默认会在脚本同级目录下生成 __pycache__ 目录与 .pyc 编译文件。沙箱刚上线时,普通代码一跑就抛出 PermissionError: [Errno 30] Read-only file system。解决方法非常简单:在沙箱基础镜像中强制注入环境变量 PYTHONDONTWRITEBYTECODE=1,命令解释器彻底禁止在本地生成任何缓存文件,直接在内存中编译执行。

    第二个坑是 数据分析库的临时文件依赖。业务方经常需要使用 matplotlib 和 pandas 导出图表。这些库在底层会尝试在 /tmp 创建字体缓存或临时分块文件。如果完全没有可写目录,脚本就会崩溃;但如果直接放开宿主机可写目录,又会带来逃逸风险。我们的解法是利用 Linux 的 –tmpfs 机制:给沙箱挂载一个挂载选项为 rw,noexec,nosuid,size=32m 的内存临时文件系统。允许它存取临时图表,但通过 noexec 明确禁止任何人在 /tmp 目录下执行可执行二进制或 Shell 脚本,杜绝恶意脚本下载第三方木马并在临时目录执行。

    总结

    在智能体开发中,功能往往走在安全前面,但安全才是一票否决权。不要相信大模型的“向善”对齐,更不要相信用户的输入是无害的。将所有外部生成的代码视作攻击载荷,利用 Linux 内核最坚固的 Namespaces、cgroups 与只读文件系统为它焊死铁笼,是保障生产系统夜夜能睡安稳觉的唯一工程底线。

    赞(0)
    未经允许不得转载:171主机测评 » 生产沙箱逃逸防御:如何安全执行大模型生成的 Python 代码与 Shell 命令
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址