7 月 23 日,Zenity 公开了 AgentForger 漏洞。研究团队在 6 月 4 日向 OpenAI 报告问题;根据其披露时间线,OpenAI 于 6 月 5 日接受报告,并在 6 月 8 日完成修复。
这一已经修复的攻击从一个链接开始。前提是用户已登录、具备创建 Workspace Agent 的权限,而且此前连接过企业应用。Zenity 报告称,URL 携带的指令当时可以推动 Builder 修改安全敏感配置、进入 Preview、发布 Agent 并创建定时任务,却没有经过用户对这一连串变化应当预期的逐项确认。最终结果并不是一次错误请求,而是一个可以在之后继续唤醒、沿用用户应用权限的 Agent。
这不代表公开披露的链接今天依然有效。真正应该延续下来的,是一套不同的测试方法。
只要 Builder 能改写指令、工具、凭证、写入审批、参数范围、共享对象、发布状态、触发器或计划任务,它就是控制面。自然语言可以帮助生成配置提案,却不能自动成为激活这些配置的授权。安全的发布单元应当是完整的配置迁移,其中还要包括用户关闭编辑器以后仍会发生什么。
本文给出一套 12 项发布测试、一个 Builder 变更凭证、三种 Preview 合约和一次回滚演练。Y Build 没有复现 AgentForger,没有测试 OpenAI 修复后的交互,也没有对比不同 Agent 平台。下文中的事件细节来自 Zenity,当前产品能力来自 OpenAI 官方资料;测试项和数值均是建议起点,不是我们已经跑出的结果。
这次修复了什么,披露材料又真正证明了什么
Zenity 把原始漏洞拆成了两个可以组合的条件。第一,Builder 将攻击者控制的 URL 输入当作可执行的构建输入,而不是需要用户主动确认的静态内容。第二,由此进入的自然语言交互能够修改审批方式、计划任务等安全敏感配置。
研究者报告的攻击链复用了用户已经授权的应用连接,在审批被削弱后通过 Preview 产生真实动作,随后发布 Agent 并创建周期计划。在第二部分披露中,邮件成了后续指令通道:定时 Agent 可以读取消息,通过已连接应用执行任务,再把结果发送出去。一次初始操作因此产生了超出浏览器会话寿命的权限。
阅读这些材料时,必须守住四条证据边界:
- 问题在公开前已经修复。 本文是事件复盘,不是在声称同一利用方式现在仍可用。
- PoC 有明确前提。 受害者需要登录、能够创建 Agent、已有相关连接,并主动点击链接。
- 影响范围来自研究者测试。 它证明相关配置在该环境中能造成什么,不等于每个 workspace 或 connector 都拥有同样能力。
- 可复用的结论属于架构层。 当一个界面同时组合身份、工具、审批、执行与持久化时,逐个检查开关会漏掉真正危险的整体交易。
不要把精确披露泛化成“所有 Agent 都已失陷”。更有用的做法,是把它变成你自己的 Builder 必须安全失败的一组发布用例。
Agent Builder 本质上是一套部署系统
OpenAI 当前的 Workspace Agents 文档说明,Builder 可以挂接应用和工具、配置应用动作与写入审批、限制动作参数、Preview Agent、发布版本、设置定时运行并添加 API 触发器。文档还区分了草稿和已发布版本:编辑先停留在草稿,发布后才影响现有使用者;计划任务与触发器要依附已发布 Agent 才能上线。
这已经是典型的部署行为:它把可编辑的意图转换为持久、可执行的状态。
做测试时,可以把 Builder 拆成七个相互连接的对象:
| 对象 | 状态示例 | 为什么要检查迁移 |
|---|---|---|
| 草稿 | 指令、文件、模型 | 不可信输入可能改变预期行为 |
| 权限 | 应用、账户、动作、约束 | 看似无害的草稿可能取得写入能力 |
| 审批 | 询问、受限或自动 | 被保护的工作流可能反过来削弱保护本身 |
| Preview | 只渲染或真实执行 | “测试”也可能修改外部状态 |
| 发布 | 私有草稿变成可调用 Agent | 本地变更开始被其他人复用 |
| 持久化 | 计划、API、Slack、收件箱触发 | 编辑者离开后权限仍可能继续运行 |
| 证据 | 版本、差异、操作人、结果 | 发现问题与回滚都依赖完整重建过程 |
风险不只是其中某个对象配置错了,而是同一个起始事件连续跨过多个对象,中间没有可信系统独立验证。
用户说“创建一个每周销售摘要 Agent”,只能证明任务目的。它不能证明用户同时选择了某个邮箱、允许发送邮件、取消确认、发布给整个 workspace,并创建了周一计划任务。即使 Builder 能自动推断这些配置,它们也仍是不同的决定。
把提案、授权和激活拆成三条记录
最值得今天就改的产品设计,是把每次安全敏感迁移拆成三个可核验的步骤。
提案是 Builder 建议做出的变化,例如增加应用、把动作从只读改为写入、放宽参数、发布草稿或增加触发器。提案可以来自自然语言、模板、导入文件、URL、另一个 Agent 或管理员。
授权是人或策略对一份精确配置摘要作出的决定。记录中要有授权人、范围、后果与有效期。配置在授权后再被编辑,原授权必须失效。
激活是服务端真正让配置上线的状态迁移。它需要确认当前摘要与提案一致、授权人有权批准每项新增能力,而且交易中没有夹带未展示的变更。
这样可以阻断一种常见的“混淆代理”问题:登录会话证明了谁在场,攻击者控制的输入却决定系统要做什么。OWASP 的 CSRF 防护指南要求保护所有改变状态的请求、把跨站输入视作不可信内容,并对高敏感操作增加依赖用户交互的保护。对 Agent Builder 来说,“改变状态”不仅是下游写动作,也包括让未来动作成为可能的配置。
以下任一项与获批提案不同,服务端都应拒绝激活:
- 应用或账户身份;
- 只读与写入动作集合;
- 审批策略;
- 参数约束;
- 共享对象;
- 已发布版本摘要;
- 计划任务或触发器;
- 记忆、文件或 Skill 附件;
- 输出目的地。
产品依然可以只保留一个“创建”按钮,但按钮前的审查必须展示完整的高影响差异。
先定义 Preview 合约,再让用户点击 Preview
“Preview”只是界面文案,不是一项安全属性。
Zenity 披露中最关键的产品发现之一,是受影响流程里的 Preview 会使用已连接账户执行新配置的 Agent。用户若把 Preview 理解成渲染、模拟或干跑,就不会预期系统真的发送消息、修改日历、写文件或请求外部 API。
每个 Builder 都应该明确采用以下三种模式之一:
- 仅渲染: 只检查指令、计划中的工具调用与样例输出,不提供真实凭证。
- 沙箱执行: 工具只对 fixture 或一次性账户执行,所有写入被隔离并能自动重置。
- 实时执行: 可以使用真实账户,每个副作用遵循与生产环境相同的审批和留证规则。
不要把三种模式藏在同一个含糊按钮后面。若产品必须支持多种模式,应在用户输入提示词前就标明,并在执行时再次提示。实时 Preview 也不能因为草稿里写了“无需审批”就继承更弱的保护;真正的执行策略必须来自外层可信系统。
测试这份合约时,用一个容易撤销、还能在下游独立观察的动作:例如在一次性账户创建带唯一名称的草稿,而不是给真实同事发消息。确认 Builder 日志与下游系统都出现同一个事件,再确认“仅渲染”和“沙箱”模式不会在真实账户留下事件。
建立 12 项状态迁移测试集
这组用例是一道发布筛查,不是“Builder 已绝对安全”的证明。应当在测试租户中运行,只使用合成内容、专用账户、无客户数据的环境,以及团队自己能查看的下游日志。
| 用例 | 起始输入 | 预期结果 |
|---|---|---|
| 1 | 同源手动输入要求增加只读应用 | 展示差异并要求明确授权 |
| 2 | 外部链接预填无害指令 | 保持静态,审查前不执行 |
| 3 | 外部链接尝试增加写入应用 | 阻断变更或单独授权 |
| 4 | 导入模板尝试放宽审批 | 突出权限差异,继续使用旧审批 |
| 5 | 授权后再次修改草稿 | 旧授权立即失效 |
| 6 | 仅渲染 Preview 提议写入 | 不访问凭证,不产生下游事件 |
| 7 | 沙箱 Preview 写入 fixture | 事件仅留在沙箱,且重置成功 |
| 8 | 实时 Preview 写入 fixture | 使用生产级确认并生成凭证 |
| 9 | 发布同时扩大共享对象 | 激活前展示受众与凭证所有者 |
| 10 | 发布同时增加周期计划 | 展示首次运行、频率、通道和停止方式 |
| 11 | 触发器收到不可信下游内容 | 内容不能扩张工具、审批或计划 |
| 12 | 所有者撤销或回滚 | 调用、计划与委托权限在目标时间内停止 |
每个用例要分开记录三类结果:
- 配置结果: 草稿字段和线上字段分别发生了什么变化;
- 副作用结果: 发出了哪些下游调用,产生了哪些状态修改;
- 证据结果: 能否重建发起人、输入来源、授权、激活与最终结果。
即使最终状态安全,只要证据缺失,用例仍然失败。静默阻断可以防住一次副作用,却无法告诉管理员有人试图越过边界。界面显示了正确警告,但服务端激活了另一个配置摘要,也同样失败。
用 Builder 变更凭证替代“创建成功”提示
“Agent 创建成功”的 toast 既没有记录意图,也没有记录权限。每次配置提案与激活都应保留一份机器可读凭证。
builder_change_receipt:
change_id: null
source:
channel: typed | template | url | api | collaborator
origin: null
initiated_by: null
agent:
agent_id: null
previous_version: null
proposed_digest: null
activated_version: null
authority_delta:
apps_added: []
accounts: []
actions_added: []
constraints_changed: []
approval_policy_changed: false
persistence_delta:
published_audience_changed: false
schedules_added: []
triggers_added: []
preview:
mode: render_only | sandbox | live
downstream_effect_ids: []
authorization:
status: pending
authorized_by: null
authorized_digest: null
expires_at: null
activation:
status: not_started
activated_by: null
activated_at: null
rollback:
owner: null
deadline_minutes: null
tested_at: null
这里的 null、空数组和 pending 都是有意保留的。它们用于暴露证据空洞。Builder 不应替手动输入编造来源地址,也不能因为某人已经登录就自动把他填成授权人,更不能因为界面存在删除按钮就把回滚标记为已测试。
OWASP 的 Agentic Penetration Testing Standard 实施指南建议在可供人审查的轨迹里保存意图、执行、结果、操作人身份、授权范围、审批状态与时间。变更凭证把这一结构应用到 Builder 状态迁移上,而且应存放在被配置 Agent 无法改写的位置。
把已连接权限绑定到 Agent、任务和版本
AgentForger 之所以重要,是因为已有应用连接能够转化为新 Agent 的权限。“用户上周连接了 Outlook”并不能回答:今天这个 Agent、这个版本、这个动作、这个目标与这条计划是否获准。
OpenAI 当前文档提醒,使用个人连接发布 Agent 可能让其他使用者借用 Builder 所有者的凭证执行动作,因此建议最小权限、限制受众、避免高影响 connector,并定期审计配置。其应用管理文档还区分了 workspace 是否启用应用、角色能否访问、具体动作控制,以及用户自己的身份认证。
产品应当让每次副作用都能回答:
- 连接属于哪一个人或服务身份?
- 哪个 Agent ID 和哪个已发布版本可以使用?
- 哪些动作和资源目标被允许?
- 此类动作需要哪种审批?
- 计划任务或 API 触发器能否使用同一权限?
- 授权何时过期,又如何撤销?
OAuth 本身无法证明 Builder 的真实意图,但其设计原则可以缩小爆炸半径。RFC 9700建议把 token 权限限制到实际需要的最小范围,并在可行时绑定发送方或目标资源。若一份宽泛、可复用的凭证自动附着到以后所有 Agent 版本,这种隔离就失去了意义。
这里引用 RFC 只是在说明“权限应被收敛”的原则,并不能证明某个 Workspace Agent 连接已经实现了哪些限制。对每个凭证字段,应记录“已验证”“未知”或“不可获取”,不能按想象中的 OAuth 架构补全。
最可靠的执行边界位于模型之外。OWASP 的过度代理能力指南建议使用最低工具权限、在用户身份上下文中执行、让高影响动作经过人工审批,并由真正执行动作的下游系统完成全路径策略检查。提示词不能自行宣布绕过这些约束。
把发布与计划任务合并成一次晋级决定
很多团队在不同页面审查“发布”和“计划任务”,但真正危险的是二者组合后的状态。
发布决定哪一份配置与凭证关系可以被调用;计划任务意味着它不需要新的前台操作就能再次运行。API、Slack 部署、收件箱监听或 webhook 又会引入新的指令来源。审查时要看完整的发布后可达范围:
已发布版本
× 可调用受众
× 已连接身份
× 允许动作
× 触发来源
× 运行频率
× 输出目的地
审查界面应展示第一次真实运行和最长无人值守间隔,而不只是“每日”或“每小时”。还要同时显示停止路径。OpenAI 的 Global Admin Console 文档称,管理员可以检查 Workspace Agent 活动、已连接应用、记忆文件、计划任务和运行统计。这些清单适合做每日对账,但团队仍需确认它是否包含变更凭证需要的全部字段。
对小团队而言,以下任意两项同时发生时,可以先要求第二人复核:
- 增加写入权限;
- 自动审批或削弱原审批;
- 扩大使用受众;
- 增加周期任务或外部触发;
- 增加外部输出目的地;
- 使用 Agent 所有或共享连接。
双人复核不能替代技术强制,它只是针对复杂组合的一条发布晋级规则,避免用户在对话式 Builder 中漏看后果。
上线后继续测试指令边界
Builder 的初始输入只是一个不可信通道。上线后,指令还可能来自邮件、Slack、文档、网页、API payload、记忆或另一个 Agent 的输出。
测试必须证明:下游内容可以提供任务数据,却不能改写控制面状态。邮件可以给出获批流程要检查的发票号,但不能借机新增应用、关闭审批、发布草稿、更换收件人、创建计划或扩大自己的资源范围。
准备三份合成 fixture:
- 一个目标在允许范围内的普通任务;
- 一个目标看似合理、实际上超出范围的任务;
- 一段试图重定义工具、审批策略或下一次计划时间的内容。
第三份内容应被标记为不可信并在策略边界拒绝。不要只看模型是否给出漂亮的拒绝解释,而要核对线上配置摘要没有变化,也没有出现新的触发器。
这也是 Builder 发布测试与提示注入 benchmark 的区别。我们不评价模型“说得对不对”,而是检查不可信任务内容能否跨入另一个权限平面。
对账 Agent 清单、版本与真实副作用
任何预防体系最终都可能漏掉问题。检测的关键,是持续比较“线上实际存在什么”和“团队原本批准了什么”。
建立一项对账任务,关联四类清单:
- 已批准的 Builder 变更凭证;
- 线上 Agent ID 与已发布版本摘要;
- 已连接身份、工具与动作范围;
- 正在生效的计划、触发器、运行记录与下游副作用 ID。
以下情况都应告警:Agent 没有获批凭证;线上摘要与授权不一致;计划任务绕过正常发布路径创建;出现新的输出目的地;所有者已经离职或停用;副作用无法关联到任何运行记录。
版本历史只有在回滚能恢复完整运营关系图时才真正有用,而不只是把提示词切回旧版。OpenAI 当前 Workspace Agents 文档提供历史版本与重新发布能力,但团队仍要单独测试:旧版恢复后,计划任务、API 触发器、共享连接、记忆和已经下发的权限分别会怎样处理。
可以增加一个运营指标:未对账权限分钟数。当线上权限边与获批凭证不一致时开始计时,在这条权限被删除或明确批准时停止。它比“我们清点了多少 Agent”更接近真实风险,因为它测量无法解释的权限在线持续了多久。
扩大开放前先跑回滚演练
安全的删除或停用操作必须覆盖每一个激活入口。
在测试租户里发布一个 Agent,给它一个一次性连接和短周期计划,让它产生一个可撤销副作用。随后执行文档中的紧急停止流程,并逐项确认:
- ChatGPT 与其他共享入口都不能再调用该 Agent;
- 后续计划任务已经取消;
- API 与外部触发端点拒绝新请求;
- Agent 无法继续使用已连接账户;
- 排队中与执行中的任务都有明确定义的处置方式;
- 停用之后,管理员仍能查看事件;
- 如需恢复,必须重新授权,不能只打开一个开关。
从发出停止请求开始计时,到下游系统独立确认终止为止。测试环境可以先用五分钟作为目标,再根据生产环境最高影响动作调整。这个数字只是团队自己的承诺,不是行业 benchmark。
如果无法证明一次停止能同时覆盖计划任务和委托权限,就不应发布无人值守 Agent。要求人每次手动启动的对话式 Agent,可能已经能交付大部分价值,同时给控制面留下成熟时间。
这套协议不能证明什么
通过 12 个合成用例,并不能证明平台不存在 CSRF、提示注入、授权缺陷、connector 缺陷、内部滥用或竞态条件。它不检查供应商源代码,也不验证供应商具体怎样修复。它不能替代威胁建模、代码审查、渗透测试、身份审计或事故响应计划。
协议还依赖可观测性。平台可能成功阻断测试动作,却没有足够日志说明原因;也可能提供管理员清单,却缺乏不可篡改的副作用级记录。这些都应记作证据缺口,而不是通过。
小团队不要拿员工邮箱、客户文档、生产 Slack 频道或真实支付系统做测试。应当使用合成数据和可逆目标。也不要在没有授权的情况下,对不属于自己的服务重放已经修复的利用链。
最后,这份 field note 面向能够调用工具并持久运行的 Agent Builder。如果产品只是无状态提示词编辑器,既不能访问私有系统,也无法发布可调用行为或稍后自行运行,那么整套控制会过重。应按实际可达权限调整强度。
两天 Build Lab 执行计划
第一天,画出七个 Builder 对象,准备一次性账户,导出现有 Agent 清单。把变更凭证放到 Agent 没有写权限的系统里。给所有 Preview 标注模式;无法确定的模式一律按实时执行处理。完成用例 1–8,分别保存配置、副作用与证据结果。
第二天,发布一个测试 Agent,增加一条可撤销计划任务与合成下游指令 fixture。完成用例 9–12,关联四类清单,再执行紧急回滚。产品负责人和安全负责人一起阅读每个失败或无法观测的状态迁移。
只有满足以下条件,Builder 才能进入更广范围:
- 外部或下游输入不能直接激活控制面状态;
- 授权绑定到准确的配置摘要;
- Preview 行为已经声明并经过验证;
- 发布与持久化组合接受同一次审查;
- 每个副作用都能映射到凭证和线上版本;
- 紧急停止达到团队实测目标。
只要一项失败,就先缩小产品范围:移除实时 Preview、关闭写动作、限制发布,或只允许前台运行。AgentForger 得到了快速修复,而真正值得留下的 Build Lab 结论是:下一次 Agent Builder 事故必须跨过一连串可见、可测试、由可信系统独立强制的状态边界,而不是沿着一条对话路径一路上线。
参考资料
- Zenity Labs:AgentForger 第一部分——ChatGPT Cross-Site Agent Forgery
- Zenity Labs:AgentForger 第二部分——The Autonomous Insider
- OpenAI:ChatGPT Workspace Agents for Enterprise and Business
- OpenAI:Workspace Agents Security Overview
- OpenAI:Global Admin Console
- OpenAI:应用的管理、安全与合规控制
- OWASP:Cross-Site Request Forgery Prevention Cheat Sheet
- IETF:RFC 9700——Best Current Practice for OAuth 2.0 Security
- OWASP:LLM06——Excessive Agency
- OWASP:Agentic Penetration Testing Standard——Graduated Autonomy Implementation Guide