被混淆的代理人。
你为「阻止智能体做不被允许的事」而装上的每一项控制,在那种「它从头到尾什么都没越权」的攻击面前都是无用的。在一次「被混淆的代理人」攻击里,智能体是被授权的、动作是被许可的、审计日志是干净的,而损害已经完成——因为权限由你的智能体提供,而目标由一个网页提供。能找出这类问题的追问不是「这个智能体被允许做什么」,而是「它所作用的那个东西的名字从哪来」;只要答案和权限的来处不是同一个地方,你就有一个。
给这个问题起名的,是一个 1970 年代的 bug。
Norm Hardy 那篇三页的短文《The Confused Deputy(或:权能机制为何可能正是为此而被发明)》1988 年发表于 ACM SIGOPS Operating Systems Review,记的是他在 Tymshare 的一台 PDP-10 上亲眼见过的一次失败。当时的 Fortran 编译器 (SYSX)FORT 会统计客户用了哪些语言特性,并把统计写进 (SYSX)STAT。为了让它能这么做,管理员给编译器发了一张 home files license:可以往 SYSX 目录里写。
这个编译器同时还接受用户给的一个可选文件名,用于输出调试信息。一位注意到系统计费日志就放在 (SYSX)BILL 的客户,把这个名字当作调试输出的目的地传了进去。编译器把它打开了——用的是自己那张许可,因为那就是它手上有的许可——然后用编译器诊断信息覆盖掉了计费记录。
要读的是结构,不是轶事。任何一个动作都有两个输入:
- 权限(authority)——回答「这个主体被允许做这件事吗?」在这里它来自编译器的许可。
- 指名(designation)——回答「作用在哪个对象上?」在这里它来自调用方的参数。
没有任何人越过自己的权限。那位客户本来没有写计费文件的权利,也始终没有取得;他只是安排了一个确实有这项权利的东西,替他去写。代理人没有被攻破。它只是搞混了自己在替谁跑这趟差。
这正是那篇文章的副标题要谈权能机制的原因。在一个权能系统里,你根本无法指名一个你无权访问的对象——名字就是权限,于是权限与指名一同抵达,不可能被错配。STEP 4 里的每一项缓解措施,都是在一个当初并非如此构建的系统里,把这条性质找回来的办法。
智能体是「被混淆的代理人」,这是构造使然,不是意外。
Hardy 那个编译器还得有人刻意递给它一个坏文件名。智能体则是主动去找这些名字的。把那两个输入再拆一次,默认架构就相当骇人:
- 权限来自你。已登录的浏览器配置文件、MCP 配置里的 OAuth 令牌、容器上的云角色、仓库的写入范围。全都是常驻且从未被列举的——这就是环境权限,也正是一个被搞混的智能体的爆炸半径是「这个会话能摸到的一切」、而不是「你注册的那十二个工具」的原因。
- 指名来自它读到的内容。issue 评论里的 URL、README 里的路径、PDF 里的账号、HTML 注释里的指令、商品页上的评论文字。智能体的全部价值主张,恰恰是它会从自己无力核验的材料里接受目标。
提示注入是指名被偷运进来的方式;「被混淆的代理人」才是它为何要紧。把这两个概念分清在实践上很有用,因为它告诉你:降低注入成功率,和修掉这一整类漏洞,是两个不同的项目——而这一类比语言模型早了三十年。它的别名你早就认识:
- CSRF。浏览器是代理人,你的会话 cookie 是权限,而攻击者的页面提供指名。
- SSRF。服务器是代理人,它在网络内部的位置是权限,而用户提供的 URL 是指名——这正是一个带
fetch工具的智能体的形状。 - 令牌透传。一个网关把收到的令牌转发给并非该令牌签发对象的上游,就是一个代理人拿着别人的权限、去打自己选的目标。
令人不安的推论是:一个运转得完美无缺的模型是最危险的代理人,因为这类攻击需要它称职且服从,而不需要它出故障。关于「这件事的激励面版本」,见委托—代理问题。
四项感觉像解法、其实不是的控制。
它们每一项都因为别的理由值得拥有。但没有一项能对付「一个在权限内行事的代理人」。
- 更强的智能体身份。工作负载身份、短寿命证书、签名请求——都好,也都正交。那个代理人本来就已通过认证。更严格地证明它是哪一个代理人,并不能告诉资源方它正在替谁跑差。
- 确认弹窗。一个写着「允许该智能体写入
invoices/bill.csv吗?」的弹窗,是在请用户为攻击者的指名背书。弹窗只能呈现智能体请求的那个动作,而智能体是诚心诚意请求了错的那个。更糟的是,审批疲劳会让第一百个弹窗比第一个更廉价。 - 提示词加固。系统提示词里的规则与指令层级能把比率压下去。它们改不了这一类,因为模型被要求在「数据与指令是同一串字节」的通道里把两者分开。降低比率是风险管理,不是调解。
- 审计日志。日志会忠实地记下:一个被授权的主体,对一份它确有权限的资源,执行了一个被许可的动作。那是一份关于「一次成功攻击」的真实记录;引用监视器那项检验说明了原因:位于混淆下游的控制会继承这份混淆。
一条好用的判据:如果你的缓解措施在「由攻击者挑目标」的情况下依然算满足,那它就不是针对这一类的缓解措施。把这句话逐条套到上面四项上。
解法是让权限与指名一同移动。
你没法让模型在「该信哪个文件名」上长出更好的判断力。你能做的是:让那个不可信的文件名本身不再授予任何东西。有四种形状能做到,按所需功夫由浅入深:
- 发句柄,不发字符串。一个参数是「由你自己的代码为某一个对象铸出的不透明标识符」的工具,无法被智能体读到的任何东西指向第二个对象;一个参数是路径、URL 或账号的工具则可以。这是工具设计里单项杠杆最大的改动,而它是一个 schema 决策,不是一项安全特性。
- 把凭据的范围收到任务上,而不是收到智能体上。一个只对那一个文件、只在几分钟内有效的预签名 URL;一个受众就是它将被出示给的那唯一上游的令牌;一个只对一条分支有写权限的仓库令牌。手上没有大权限的智能体,也就没有大权限可供别人劝它去用。
- 交换令牌,绝不转发。在每一跳都铸出一张新凭据,绑定真实的终端用户与真实的目的地。一张被转发的令牌,就是那张计费文件许可,在每一层重演一遍。
- 把读的权限与写的权限分开。两个身份、两条网络通路。取回那张不可信网页的身份,不应该是能照着网页所说去动手的那个身份。这是同一招的架构版本,也正是让一次成功注入的爆炸半径有限的东西。
当以上都做不到时——一个只收路径的老工具,加一张无法再收窄的凭据——退回去约束指名本身:一份在读取不可信内容之前就算出来的目标白名单,在模型之外强制执行。那更弱,但它仍然是调解,而且严格优于「请智能体多提防一点」。
今天下午就把这次盘点做掉,因为它只要一个小时,而且是真能挖出 bug 的那一次。把你的智能体能调用的每一个工具列成一张表,两列:权限从哪来,以及目标从哪来。两者来源不同的每一行,都是一个「被混淆的代理人」;其中目标来自「抓回来的内容」的那些行,是最该先修的。多数团队发现自己最糟的那一行是某个 fetch、某次文件写入,或者某条接受自由文本标识符的数据库查询。