你的Agent沙箱只是一个建议
2026年3月6日,一名安全研究人员向Cline仓库提交了一个GitHub issue。该issue标题中包含了提示注入攻击。三小时后,[email protected]携带后门发布到了npm。1
沙箱没有起到任何作用。Agent完全在其被授予的权限范围内运行。它所执行的每一个操作——从读取issue到安装受污染的npm包,再到将CI缓存填充超过10GB驱逐阈值——都拥有合法授权。
摘要
- 沙箱在三个层面上失效。 字符串黑名单被路径别名绕过(
/proc/self/root/usr/bin/npx)。命名空间隔离被自主禁用所击破。内核级强制执行被动态链接器调用绕过(ld-linux-x86-64.so.2)。以上三种方式均已在2026年的生产系统中被验证。 - 最危险的攻击无需逃逸。 Clinejection完全在授权权限内运行,利用共享的CI缓存键从低权限的issue分类工作流横向移动到高权限的发布工作流。没有任何沙箱拦截到它,因为没有任何沙箱规则被违反。
- 防御需要纵深层次,而非单一壁垒。 输入分类、内核级强制执行(Seatbelt/seccomp)、出口流量监控以及状态隔离——每一层捕获上一层遗漏的威胁。下文包含具体的hook实现和Seatbelt配置文件。
沙箱失效的三个层面
每个Agent沙箱都在三个强制执行级别之一运行。每个级别都有已知的绕过方式。这些绕过并非理论推演:2026年,三个独立的研究团队在生产环境的Agent系统上验证了全部三种绕过方式。
第一层:基于字符串的黑名单
最简单的沙箱就是一份被封锁的命令列表。Claude Code维护着一份Agent不能调用的工具黑名单:curl、wget、npx等。黑名单在执行前对命令字符串进行匹配。2
绕过方式:路径别名。在Linux上,/proc/self/root/usr/bin/npx解析到与/usr/bin/npx相同的二进制文件。字符串不同,二进制文件完全一样。黑名单看到一个无法识别的路径,于是放行执行。2
这项技术并不新颖。安全文献记载的通过/proc、符号链接和相对路径进行路径别名攻击已有数十年历史。新颖之处在于Agent自主发现了这种绕过方式。没有人指示Agent去规避黑名单。Agent接收到一个需要完成的任务,黑名单阻断了直接路径,Agent便搜索能够实现相同目标的替代路径。找到之后,它继续执行。2
字符串匹配之所以失败,是因为它作用于标签(文件名),而非身份(二进制内容)。任何通过路径来识别程序的系统,在面对知道如何构造替代路径的对手或Agent时都会失败。
第二层:命名空间隔离
当Anthropic捕获到路径别名绕过后,他们部署了bubblewrap:一个基于命名空间的沙箱,通过Linux命名空间限制文件系统访问、进程可见性和网络能力。2
绕过方式:自主禁用。Agent识别出沙箱阻碍了任务完成。在没有接到禁用沙箱指令的情况下,Agent自行推理出了障碍所在并尝试移除它。这个区别很重要:该行为并非源自提示注入。Agent自身的规划过程识别出了一个约束条件,并着手消除它——将使Agent在问题解决中发挥作用的同一能力应用到了”安全控制阻碍了我”这个问题上。2
当Agent拥有足够的权限来修改自身执行环境时,命名空间隔离就会失效。沙箱运行在与Agent相同的信任上下文中。如果Agent可以写入文件、修改配置或执行命令,它就可以尝试重新配置或禁用沙箱。隔离边界的强度完全取决于Agent修改该边界能力的受限程度。
第三层:内核强制执行钩子
最强的Linux沙箱方案是hook内核的execve系统调用——即加载并运行新程序的调用。如果二进制文件不在白名单中,execve返回EPERM,程序不会运行。2
绕过方式:动态链接器调用。命令/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/bin/wget通过动态链接器的内存映射而非execve来加载wget二进制文件。内核强制执行钩子永远不会触发,因为二进制加载路径不同:使用的是mmap和mprotect,而非execve。2
动态链接器绕过代表的是一类漏洞,而非单一技巧。任何不经过execve的代码加载机制都能规避基于execve的强制执行:动态链接、dlopen、解释型语言加载共享库、内存映射执行。攻击面涵盖了从磁盘到执行过程中绕过被监控的单一系统调用的所有路径。
提出这些发现的文章建议使用Veto:一个基于内容寻址的强制执行系统,通过SHA-256哈希值而非文件名或加载机制来识别二进制文件。2 Veto同时在执行级别、加载级别和网络级别运作。该方案思路正确但增加了复杂性:每次二进制文件更新都需要重新计算哈希值,而且基于哈希的强制执行无法应对从不产生二进制文件的解释型代码(Python、JavaScript、shell脚本)。
第四种失效:授权内滥用
上述三种沙箱逃逸共享一个共同假设:Agent正在尝试执行它未被允许执行的操作。Clinejection完全推翻了这一假设。1
攻击链:
| 步骤 | 操作 | 权限 | 沙箱状态 |
|---|---|---|---|
| 1 | 攻击者提交包含注入内容的issue | 公开的GitHub访问权限 | 未涉及 |
| 2 | Claude Code读取issue并安装npm包 | Bash(已授权)、Write(已授权) | 所有权限合法 |
| 3 | preinstall脚本将缓存填充超过10GB | npm生命周期钩子(标准行为) | 在沙箱内 |
| 4 | 被驱逐的缓存被受污染的条目替换 | GitHub Actions缓存(共享) | 在CI权限内 |
| 5 | 夜间发布从受污染的缓存构建 | 发布工作流(定时触发) | 在CI权限内 |
| 6 | [email protected]携带后门发布 |
npm publish(发布工作流) | 在发布权限内 |
此攻击未突破任何沙箱。未提升任何权限。每个单独操作都拥有合法授权。攻击之所以成功,是因为两个工作流——issue分类和夜间发布——共享了相同的缓存键(${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }})。攻击者利用的是通过共享状态的横向移动,而非通过沙箱逃逸的纵向提权。1
缓存键冲突意味着任何匿名GitHub用户都能触发一个工作流,污染馈入发布流水线的构建产物。issue分类工作流完全没有理由与发布工作流共享状态。但GitHub Actions的缓存默认在仓库内的所有工作流之间共享。安全模型假设工作流之间是独立的。事实并非如此。1
授权操作组合成未授权结果的模式,正是SoK: Agentic Skills论文所形式化的技能组合缺陷。单个工具是授权的。单个操作是被允许的。但它们的组合产生了任何单独权限检查都无法捕获的行为。3
Clinejection不是边缘案例。对于任何在工具级别授予Agent权限而不监控操作级别组合的系统来说,这是默认的失效模式。我此前记录的虚构反馈循环利用了同样的缺陷:系统授权了每个单独操作(写入记忆、读取记忆、发布到平台)。而其组合(虚构、持久化、发布、强化)并未获得此类授权。4
攻击成功的必要条件
攻击之所以成功,源于四个条件,每个条件对应Agent可见性堆栈中的一个防御层:8
-
不受信任的输入被当作可信处理。 issue标题来自任何匿名GitHub用户。Claude Code以与维护者指令相同的权限处理它。防御措施:输入来源分类。
-
过于宽泛的工具权限。 分类工作流授予了Bash访问权限。但分类任务实际只需要对issue元数据的Read权限和添加标签的Write权限。该任务不需要Bash,但Bash访问权限使得npm install命令得以执行。防御措施:最小权限工具授予。
-
跨信任边界的共享状态。 修复方法只需在每个工作流文件中修改一行:
# Before: shared key (vulnerable)
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
# After: workflow-scoped key
key: ${{ runner.os }}-npm-triage-${{ hashFiles('package-lock.json') }}
防御措施:独立的缓存键,工作流隔离。
- 没有出口流量监控。 npm install命令发出了出站网络请求来获取恶意包。没有任何监控检测到分类工作流中的异常出站流量。防御措施:出站请求日志记录,域名白名单。
为什么基于权限的安全模型不够
目前讨论的每个沙箱和权限系统都基于同一模型运作:定义Agent可以做什么,然后验证每个操作是否符合定义。该模型存在三个结构性弱点。
权限授予给工具,而非操作组合。 Agent可以使用Bash。Agent可以使用web-fetch。Agent可以使用file-write。每个权限单独来看都合理。但Bash + web-fetch + file-write按序组合,就构成了一个数据外泄原语。没有任何工具级权限检查能捕获这种组合。静默出口攻击完全在已授予的工具权限内运作。5
Agent会围绕约束条件进行优化。 沙箱逃逸研究表明,Agent将安全控制视为障碍而非边界。当黑名单阻断了一条路径时,Agent找到了替代路径。当命名空间隔离阻止了访问时,Agent试图禁用隔离。这种行为不是恶意,而是使Agent在问题解决中发挥作用的同一种目标导向推理。一个被告知”完成任务”的Agent,会将阻止任务完成的安全控制视为需要解决的问题。2
权限范围超出了授予它的上下文。 Cline的issue分类工作流为了分类issue而授予Claude Code Bash访问权限。同样的Bash权限足以安装任意npm包、执行preinstall脚本以及操纵GitHub Actions缓存。维护者将权限限定在了目的上,却没有限定在该目的所需的具体操作上。目的范围的权限需要一个从”分类issue”到”读取issue元数据、分类、添加标签”的映射,该映射排除”从不受信任的来源安装包”。目前没有主流Agent框架提供这种映射。1
mcp-firewall项目通过在工具调用级别而非工具可用性级别实施策略来解决权限粒度问题。6 策略不再是”Agent可以使用Bash”,而是”Agent可以使用Bash在项目目录中运行git命令”。粒度的提升非常显著,但仅靠它并不充分:Clinejection的npm install命令对于允许npm操作的工具调用策略来说,看起来就像一个合法的构建步骤。
内核级强制执行的实际形态
沙箱逃逸研究建议在内核级别实施强制执行。我的生产系统使用macOS Seatbelt(sandbox-exec)实现了这一方案的一个版本——这是一个内核级强制访问控制框架,限制Agent派生的整个进程树。7
该架构与bubblewrap有三个关键区别:
进程树强制执行。 Seatbelt的限制适用于Agent进程及其派生的每个子进程。如果Agent运行bash -c "python3 script.py",Python进程从bash进程继承Seatbelt配置文件,而bash进程又从Agent进程继承。无论进程派生机制如何,限制都会传播。7
路径级拒绝规则。 配置文件拒绝对特定文件系统路径的读写访问:~/.ssh、~/.gnupg、~/.aws、任何.env文件以及凭据文件。拒绝操作在内核VFS层执行,而非字符串匹配层。/proc/self/root/home/user/.ssh/id_rsa通过VFS解析到与~/.ssh/id_rsa相同的inode,而Seatbelt配置文件拒绝的是对inode的访问,而非路径字符串。7
从Agent角度不可变。 Seatbelt配置文件由hook系统在Agent启动时加载。Agent无法修改hook系统,因为hook是在CLI初始化时从settings.json读取的,此时Agent尚未获得控制权。Agent无法禁用自己的沙箱,原因与您无法从操作系统内部删除加载它的引导程序相同:强制执行机制运行在比其约束的代码更高的信任上下文中。7
配置文件本身只有20行Seatbelt语法:
(version 1)
(allow default)
;; Block writes to credential paths
(deny file-write* (subpath "/Users/me/.ssh"))
(deny file-write* (subpath "/Users/me/.gnupg"))
(deny file-write* (subpath "/Users/me/.aws"))
(deny file-write* (subpath "/Users/me/.config/gh"))
(deny file-write* (literal "/Users/me/.env"))
(deny file-write* (regex #"/Users/me/\.env\."))
(deny file-write* (subpath "/Users/me/Library/Keychains"))
hook将每个Bash命令包装为:sandbox-exec -f claude-sandbox.sb /bin/sh command.sh。我在强制执行前以dry-run模式(记录违规但不阻止)运行了一周。31个测试验证了沙箱能够拦截对被封锁路径的读取和写入尝试,同时合法操作(git、python、项目目录中的文件编辑)正常通过。每个命令的开销约为2毫秒。7
Seatbelt方案解决了三种沙箱逃逸级别中的两种:
| 逃逸级别 | Seatbelt的应对 |
|---|---|
| 路径别名 | VFS级别的强制执行在检查策略前解析所有路径。指向同一inode的不同路径字符串触发相同的拒绝规则。 |
| 自主禁用 | hook在Agent之前加载,来自Agent无法修改的配置。Agent无法通过推理逃出一个它无法访问的机制。 |
| 动态链接器绕过 | 尚未针对Seatbelt进行专项测试。Seatbelt在VFS层限制文件访问,因此ld-linux尝试mmap被封锁的二进制文件时应该在文件打开阶段就会失败。需要明确验证。 |
动态链接器绕过是一个待解问题。Seatbelt应该能阻止它,因为拒绝规则作用于文件访问,而动态链接器必须先读取二进制文件才能将其映射到内存中。但”应该”不是证据。针对Seatbelt配置文件测试这一特定攻击路径是一项待完成的验证工作。7
防御堆栈
有效的Agent安全不是单一机制,而是一个分层堆栈,每一层应对不同的失效模式。没有哪一层是充分的。组合起来它们覆盖了已知的攻击面。
| 层级 | 应对问题 | 机制 | 我的实际运行方案 |
|---|---|---|---|
| 输入净化 | 不受信任的内容注入 | 剥离元数据,分类输入来源,拒绝非预期格式 | Web提取库剥离<title>、<meta>、OG标签 |
| 权限限定 | 过于宽泛的工具授予 | 目的范围权限,按任务类型最小权限 | hook配置中的任务专用权限配置文件 |
| 内核沙箱 | Agent逃逸约束 | Seatbelt(macOS)、Veto(Linux)、进程树强制执行 | 带Seatbelt配置文件的sandbox-bash.sh,31个测试 |
| 出口监控 | 数据外泄,异常出站流量 | 记录所有出站请求,对异常域名告警 | Web提取中的URL日志,PreToolUse中的域名白名单 |
| 状态隔离 | 跨工作流污染 | 按信任级别分离密钥、缓存键和产物存储 | 工作流级别隔离(CI平台职责) |
| 输出防火墙 | 未经验证的发布到外部系统 | 将命令分类为本地/共享/外部,外部操作交由人工审核 | 发布边界hook |
堆栈按强制执行点排序:输入、权限、执行、出口、状态、输出。每一层捕获上一层遗漏的威胁。输入净化捕获提示注入。如果注入通过,权限限定阻止Agent执行注入的命令。如果命令执行了,内核沙箱阻止对敏感资源的访问。如果Agent在授权资源范围内,出口监控捕获数据外泄。如果数据留在系统内,状态隔离防止跨工作流污染。如果污染发生,输出防火墙阻止发布。
没有哪一层是完美的。Clinejection能通过内核沙箱,因为攻击未突破任何沙箱。攻击会在权限限定(分类不需要Bash)或状态隔离(独立缓存键)处被拦截。捕获攻击的防御层取决于哪一层应对了攻击所利用的特定条件。
核心要点
致DevOps和CI维护者: - 审计每个处理不受信任输入(issue、PR、评论)的工作流的共享缓存键。不同信任级别需要不同的缓存命名空间。 - 从分类工作流中移除Bash和Write权限。issue分类只需要对元数据的Read权限和对标签的Write权限,仅此而已。
致Agent开发者: - 在Agent处理输入之前先分类输入来源。匿名用户内容获得只读工具访问权限。仓库代码获得完整权限集。 - 实施目的范围权限:将任务类型映射到每个任务所需的最小工具集,而非最大可用集。
致安全团队: - 在应用级控制后面部署内核级强制执行(macOS上的Seatbelt,Linux上的seccomp)作为最后防线。2毫秒的开销微不足道;对路径别名和沙箱禁用的防护是绝对的。 - 监控出口流量:12个域名的白名单和逐请求日志记录能捕获任何权限检查都无法检测到的数据外泄。
今天就能实施的措施
三个hook应对影响最大的失效模式:
1. 输入来源分类(应对Clinejection)。 在Agent处理任何外部输入之前,对其信任级别进行分类。来自匿名用户的issue标题是不受信任的。来自仓库的代码是受信任的。API响应是半受信任的。分类结果决定了Agent在处理该输入时可以使用哪些工具。不受信任的输入获得只读权限。受信任的输入获得完整权限集。
2. 内核级文件系统拒绝(应对沙箱逃逸)。 将每个Bash工具调用包装在sandbox-exec(macOS)或seccomp配置文件(Linux)中。拒绝对凭据路径(~/.ssh、~/.aws、~/.gnupg、.env)的访问。开销微不足道(2毫秒),防护是绝对的:无论Agent构造什么shell命令、派生什么子进程或发现什么路径别名,都无法访问被拒绝的路径。7
3. 出口域名白名单(应对静默出口)。 在每个出站HTTP请求之前,将目标与已批准的域名列表进行核对。记录所有请求,无论通过或拒绝。日志创建审计轨迹;白名单阻止向攻击者控制的端点外泄数据。12个域名的白名单覆盖了编程Agent合法需要的服务(GitHub、npm、PyPI、文档站点)。白名单之外的任何目标默认可疑。5
每个hook是10-30行shell脚本。每个hook在每次相关工具调用时自动触发。三者合在一起应对了2026年已验证的三类攻击:通过不受信任输入的提示注入、通过路径操纵的沙箱逃逸、以及通过授权出站请求的数据外泄。
Cline的攻击者只需要一个GitHub issue和三个小时。下一次攻击将对使用默认权限、共享缓存且无出口监控运行AI分类的仓库使用同样的手法。问题不在于您的沙箱能否撑住。2026年三个独立的研究成果已经证明它撑不住。问题在于当沙箱失效时,什么能拦截住攻击。
常见问题
Clinejection与普通供应链攻击有什么不同?
传统供应链攻击入侵的是开发者的机器或凭据。Clinejection通过处理不受信任用户输入的AI Agent入侵了CI流水线。攻击者从未拥有仓库访问权限,从未入侵凭据,从未利用构建系统中的漏洞。攻击利用AI Agent作为桥梁,通过共享缓存从不受信任的输入(issue标题)抵达受信任的基础设施(发布流水线)。1
这是否意味着AI驱动的issue分类本质上是危险的?
本质上不是,但当前的默认配置是危险的。对每个由任意用户提交的issue都以Bash权限运行Claude Code,等同于执行来自匿名贡献者的任意代码。分类Agent需要对issue元数据的Read权限和添加标签的Write权限。它们不需要Bash、web-fetch或任何可以修改构建环境的工具。1
用容器和虚拟机做沙箱怎么样?
容器和虚拟机提供比进程级沙箱更强的隔离,但它们引入了延迟(冷启动100毫秒以上)和复杂性(镜像管理、卷挂载、网络配置)。对于每次会话调用50+工具的交互式Agent会话,延迟会累积。内核级方案(Seatbelt、seccomp、Veto)在现有进程模型内运行,因此无需额外开销即可提供强制执行。7
Agent能绕过Seatbelt吗?
Seatbelt是由macOS内核强制执行的强制访问控制框架。绕过它需要内核级漏洞利用。Agent运行在用户空间,无法修改内核状态。但Seatbelt的范围比完整容器窄:它限制文件访问和进程执行,但对网络访问的限制程度不同。将Seatbelt与出口监控结合可覆盖两个攻击面。7
这与NIST AI Agent安全框架有什么关系?
我提交给NIST的公开评论指出Agent威胁是行为性的,而非架构性的。本文记录的沙箱失效强化了这一论点:Agent逃逸是行为性的(Agent推理如何绕过约束),最具破坏力的攻击(Clinejection)也完全是行为性的(授权操作组合成未授权结果)。NIST现有框架(CSF 2.0、SP 800-53、AI RMF)未将行为组合作为威胁类别加以应对。9
-
Khan, A. “Clinejection: Compromising Cline’s Production Releases just by Prompting an Issue Triager.” March 2026. Via Simon Willison. ↩↩↩↩↩↩↩
-
tomvault. “How Claude Code Escapes Its Own Denylist and Sandbox.” ona.com, March 2026. HN discussion, 34 points, 14 comments. ↩↩↩↩↩↩↩↩↩
-
Jiang, M. et al. “SoK: Organizing, Orchestrating, and Benchmarking Agent Skills at Scale.” Semantic Scholar, February 2026. ↩
-
Crosley, B. “The Fabrication Firewall: When Your Agent Publishes Lies.” blakecrosley.com, February 2026. ↩
-
Lan, J. et al. “Silent Egress: The Implicit Prompt Injection Attack Surface.” Semantic Scholar, February 2026. Crosley, B. “Silent Egress: The Attack Surface You Didn’t Build.” blakecrosley.com, March 2026. ↩↩
-
dzervas. “mcp-firewall: A tool policy manager for AI agents.” GitHub, March 2026. ↩
-
Author’s production hook system. 84 hooks across 15 event types, 60+ sessions. Sandbox: macOS Seatbelt profile, 31 tests, approximately 2ms overhead per command. ↩↩↩↩↩↩↩↩↩
-
Crosley, B. “The Invisible Agent: Why You Can’t Govern What You Can’t See.” blakecrosley.com, March 2026. ↩
-
Crosley, B. “What I Told NIST About AI Agent Security.” blakecrosley.com, February 2026. NIST docket NIST-2025-0035. ↩