2025 年以来,主流 coding agent 的漏洞通报进入高发期,Claude Code、Codex、Cursor、Gemini CLI、GitHub Copilot 相继出现可由恶意仓库远程触发的严重漏洞。Claude Code 的 CVE-2025-59536,恶意仓库携带的 hooks 配置先于信任对话框执行[1];Codex CLI 的 CVE-2025-61260,项目本地的 MCP 配置被自动加载,克隆一个仓库就交出执行权与 GitHub token[2];Cursor 的 DuneSlide(CVE-2026-50548/49),沙箱写入白名单按模型填写的参数构建,注入内容可以让白名单指向沙箱执行器本身[4]。

Gemini CLI 的 CVE-2026-12537,工作区信任判断与工具白名单被绕过,最终落到容器启动器的命令注入[5];GitHub Copilot 的 CVE-2025-53773,恶意仓库把 autoApprove 写进设置,agent 直接进入全自动模式[6]。2026 年 9 月,Codex 再披露 Heapjack 与 Overpatch 两个无 CVE 编号的沙箱逃逸漏洞,共同根因是沙箱的强制机制与被它限制的代码处在同一信任域[3]。

这些漏洞的直接根因各不相同,覆盖执行时序、配置自动加载、沙箱构建方式、信任判断与白名单解析,但失效位置集中在少数几个部件上:配置加载、权限判定、沙箱边界与框架自身代码。失效位置如此集中并非偶然,它与 agent 的内部结构直接相关。要回答“为什么是这几个位置”,需要先把 agent 拆开,弄清它由哪些部件组成、哪些输入能够进入、中间有几道关卡。

这是 AI Agent 安全系列的第一篇。系列的分析路径是:从内部结构推导攻击面,再从每个攻击面推导攻击路径,每一步用公开的 CVE 与 writeup 印证。

与传统软件的本质差别:输入参与决策

传统软件同样要处理不可信输入:浏览器解析网页,邮件客户端解析邮件,攻击者控制的内容每天都在进入系统。但传统软件的数据与程序是分离的,输入是数据,不参与决定程序下一步做什么。浏览器拿到一段 HTML,按照写定的渲染逻辑绘制页面;攻击者若想利用,必须先找到解析器或内存管理的缺陷,把数据变成控制流,这是漏洞挖掘的传统路径,门槛不低。

agent 改变了这个前提,网页内容、issue 评论、依赖包里的注释、MCP server 返回的结果,进入 agent 之后都直接参与决定下一步调用哪个工具、传递什么参数,模型把读进的一段文字转成一次工具调用,输入由此从数据变成了决策依据本身。

两种模型的差别如下图:

Traditional softwareUntrusted inputweb page · emailParserstrict formatProgram logichard-codedActionrender · computedata | controlattacker must find a parser or memory flaw to turn data into control flowAI AgentUntrusted inputweb page · issue · MCP resultModel reads contentcontent → decision inputNext tool callwhich tool · what argsboundary removedcontent directly shapes decisions — no boundary between data and control

输入参与决策是第一条事实,权限是第二条。coding agent 通常持有开发者本人的完整权限,从读写文件、执行命令到使用其凭证,这种权限是产品前提而非实现上的省事:agent 要执行业务,就必须持有相应权限。两条事实合在一起,构成 agent 安全的核心问题:不可信内容能够影响决策,决策的执行又携带完整权限,中间的防线是什么、能否有效拦截。输入与执行之间的这些防线,正是 Agent 安全研究的对象。

业界对这类风险早有概括,即 Lethal Trifecta(致命三要素)[7]:一个 agent 同时具备接触不可信内容、读取敏感数据、对外通信三种能力时,攻击者就能够诱导 agent 把敏感数据读出来、发送出去。

AttackerpayloadAgentcontext: flat token stream(no source tags)Sensitive datalocal files · credentialsrepo secretsExternal channelHTTP · comment · PR132untrusted contentissue · web · MCP resultexfiltrationreadAll three present: attacker's content in, your data out, one session

Trifecta 的三个条件与前文两条事实完全对应:不可信内容影响决策对应第一条,完整权限覆盖后两条(敏感数据可读、数据具备外发通道)。它回答的是攻击链能否成立;至于从哪里攻破、防线为什么拦不住,它没有回答,而这两问是后续各篇的主题,最后一篇会回到这里。

AI Agent 的结构剖析

从整体结构看,一个 agent 运行时,从用户按下回车到命令真正执行,请求依次经过六层;六层之外另有配置面与沙箱两个部件,配置面不在请求通路上,但工具注册、权限规则与运行参数都由它下发,六层的运行方式由它决定;外部环境经内容、服务与文件三条通道接入。总体结构与数据通路如下图:

AgentUsertask / renderL1 UIinput · approval · renderingL2 Orchestrationmain loop · context · sub-agentsL3 Modelreasoning · statelessL4 Permission Checkallow / ask / denyL5 Toolsread / write / exec / networkL6 Memory + Storagememory · logs · credentialsSandboxfile · process · network boundariesread / writeall execactions: write · exec · network → external systemsask / approvememory recallEXTERNALENVIRONMENTContentreadServiceconnect · invokeFileloadchannelsCONFIGHookstrigger commandsPoliciesallow · denyComponentsMCP · pluginsParamsmodel · keyspolicyregisterhooks · no checksandbox policyescalate = swap profile

请求经过的六层

L1 界面层承载人与 agent 的全部交互。用户通过终端、IDE 插件或桌面应用提交任务,审批请求以弹窗或命令行确认框的形式呈现,执行结果也在这一层展示。同一个核心可以搭配不同的界面形态:以 Codex 为例,同一套核心同时支撑终端命令行和桌面应用,审批逻辑完全一致,差异只在于审批框由命令行还是应用窗口呈现。

L2 编排层是整个 agent 的主循环,负责维护消息历史,并在每一轮把系统提示、工具定义、对话记录与工具返回结果组装成一个请求发送给模型,同时承担上下文压缩与子 agent 的派生。所有输入,无论来自系统提示、工具定义还是外部内容,最终都在这一层汇成同一个上下文。

L3 模型层承担推理,形态为远端 API 或本地权重,每一轮推理都是无状态的,跨轮存活的上下文保存在 L2 而非模型侧。图中将其画为虚线框,因为它是整个结构里唯一不受 harness 控制的部件。harness 指包裹模型的这层代码,负责组装请求、执行工具与管理状态,业内也称为框架,全篇统一用 harness 这个词。

L4 权限判定层位于模型与工具之间:模型每一次调用工具都必须经过它,判定结果为允许、询问、拒绝三选一。判定的依据是配置面下发的权限规则与审批策略,而非模型自身的判断。

L5 工具层是 agent 唯一的动作出口,工具注册表位于这一层,模型能够调用的所有接口都在这里注册。agent 实际能做什么,取决于这一层注册了哪些工具:注册一个 MCP server,就等于为 agent 增加了一整条外部数据通路。

L6 记忆与存储层保存持久状态,包括项目记忆、全局记忆、会话记录与凭证。这些状态跨会话存活,本次会话写入的内容在下一次会话中依然存在。凭证只在这一层存放,实际使用发生在 L5 执行时。

工具效果分类与 shell 工具

工具是 agent 作用于环境的唯一途径,按效果分为四类。

1)读取类获取环境信息,是 agent 认知外部世界的通道。

2)写入类修改环境状态,覆盖从编辑文件到提交代码的全部持久化操作。

3)执行类运行代码或进程,是 agent 行为中破坏力最直接的一类。

4)通信类负责网络收发,决定 agent 能否与外部交换数据。

四类效果并不彼此独立,数据外带、C2 通道、横向移动这些攻击出口,全部由通信配合读取或执行组合完成。

shell 工具是四类效果的交汇点,也是权限判定最难覆盖的对象。一条 shell 命令可以同时完成读取文件、修改配置、执行脚本与对外通信,其参数空间是整个 shell 语言;权限判定面对它只能做词法解析,而词法解析与语义天然对不上:同一段命令可以有一百种写法,解析器看到的与 shell 实际执行的可以是两回事。大量判定绕过漏洞源于此处,行业对 shell 工具的单独对待也源于此处,命令白名单、前缀解析、强制沙箱等措施全部针对这一个工具而设。

不在请求链上的两个部件

六层之外还有两个部件,它们不在请求链上,却决定着这条链的行为。

配置面是 agent 的控制面,内容分为四类:组件注册决定注册了哪些工具,权限策略规定权限规则如何判定,事件钩子定义什么事件触发什么命令,运行参数指定模型端点与凭证的获取方式。配置生效即控制:注册了什么工具,agent 就能做什么;写入了什么权限规则,什么动作就免于确认。

配置还有一个关键性质:判断一份配置可信不可信,看的是它经过什么途径写进来,与内容本身无关,写入途径越不可信,这份配置就越不可信。同一份配置,来自产品内置默认值与来自刚克隆的仓库,信任级别完全不同。信任梯度大致为五档,从内置、企业下发、用户目录、项目目录到市场自动安装,越靠后越不可信,而 agent 最常加载的正是后面几种。产品里已有的信任确认机制,VS Code 的工作区信任、JetBrains 的可信项目、Claude Code 的目录信任确认,覆盖的都只是项目目录这一档。

沙箱是执行层面的约束边界。它不判断动作的好坏,只限制执行时能够触达的范围:可写范围多大、网络是否可用、凭证在边界内是否可见。沙箱由操作系统提供的强制机制实现,不由 harness 自己实现,包括 macOS 的 Seatbelt、Linux 的 bubblewrap 或 Landlock 加 seccomp、Windows 的受限令牌(restricted token),以及容器;harness 负责对它进行配置和调用。沙箱与 L5 的衔接在执行这一步:模型发起的命令在落到外部系统前先经过沙箱,总图上 L5 指向沙箱的 all exec 说的就是这个;写入路径的可写范围同样由沙箱策略界定。

以 Codex 桌面版为例

Codex 桌面版在 macOS 上是 ChatGPT.app,应用挂着 ChatGPT 的名字,包标识(com.openai.codex)写的却是 Codex。这个应用由闭源与开源两部分组成:Electron 壳闭源,内容都在应用包目录里,装了应用就能查看;内嵌的核心开源,代码在 openai/codex 仓库中可以直接阅读。六层中的 L2 到 L6 都落在核心上,只有 L1 属于壳。以下依据公开文档[8]、公开仓库与应用包内容逐层拆解,结构与数据通路如下图:

Agent — ChatGPT.appUsertask / renderL1 UIElectron shell — ChatGPT.appL2 Orchestrationcodex-rs core · session loopL3 Modelremote API · provider configurableL4 Permission Checkhooks → Guardian → user · approval_policy 四档L5 Toolsshell · apply_patch · web_search · MCP · code-modeL6 Memory + Storagerollout · sqlite · memories · authSandboxfile · process · network boundariesapp-server (stdio)read / writeall execexec: workspace + /tmp write · network off by defaultask / approvememory recallEXTERNALENVIRONMENTContentfiles · web pagesServiceMCP serversFileAGENTS.md · skillschannelsCONFIGHookstrigger commandsPoliciesallow · denyComponentsMCP · pluginsParamsmodel · keyspolicyregisterhooks · no checksandbox policyescalate = swap profile

任务的完整流程(桌面版)如下:

  1. 打开应用,壳启动并拉起内嵌的核心子进程。核心读配置:~/.codex/config.toml 是用户级配置,approval_policy、sandbox_mode、mcp_servers、模型 provider 都写在这里;企业可以用 managed_config.toml 或 macOS 的 MDM(企业设备管理下发通道)推配置,优先级高于用户文件与命令行参数。这对应前文信任梯度中的“企业下发”一档。
  2. 在窗口里输入任务,壳把任务递给核心,由会话主循环接管(主体在 codex-rs 的 core crate)。每一轮把系统提示、AGENTS.md、工具定义、对话历史拼成一个请求。
  3. AGENTS.md 在这一步进上下文:先读全局的 ~/.codex/AGENTS.md,再从仓库根向下走到当前目录,每层目录最多取一个文件,拼接在一起;skills(SKILL.md 提示包,附带可执行脚本)也在这一步加载,项目目录里携带的任何指令都由此进入模型上下文。
  4. 请求发往远端模型服务,模型返回工具调用。
  5. 判定。命令要过三道关卡:hooks 的 PermissionRequest 环节先行,Guardian(内置的模型自动审查,用模型给命令评分)居中,approval_policy 收尾,由它决定自动放行还是弹出审批。approval_policy 分 untrusted、on-failure、on-request、never 四档,版本控制目录默认取 Auto 档(workspace-write 加 on-request):工作区外的写入与需要联网的命令都问用户,非版本控制目录则默认只读。审批的呈现随界面而变:终端里是命令行确认框,桌面版是应用窗口里的对话框,批不批仍由核心逻辑决定。
  6. 执行。命令先放进沙箱再运行:macOS 用 Seatbelt(经 sandbox-exec 带上生成的策略),Linux 用 Landlock 加 seccomp,Windows 用受限令牌,另有防火墙与微软执行容器(Microsoft eXecution Container,微软开源的沙箱执行框架)两类后端。workspace-write 的可写范围是工作区加临时目录。命令的网络访问另成一条边界,经内置代理按域名放行,与文件边界互不替代,默认同样收紧。
  7. 工具结果拼回上下文,进入下一轮。整个过程的状态都落在 ~/.codex:会话记录(rollout 文件)与 sqlite 状态库,登录凭证与持久记忆也在这个目录下,与 CLI 共享。

桌面版相对 CLI 多出的部分基本都在 L1:图形界面、登录、内置浏览器视图、系统通知、自动更新。这一层的代码不在开源仓库中,但 Electron 应用将界面代码打包在 app.asar 里。

回到总图,逐格对上:

结构位置Codex 桌面版里的东西
L1 界面Electron 壳:Chromium 渲染进程画界面,主进程管窗口、登录、内置浏览器视图、自动更新;壳把任务递给内嵌核心
L2 编排内嵌核心(codex-rs 的 core crate)里的会话主循环;AGENTS.md 在组装请求时进上下文
L3 模型远端模型服务(默认 OpenAI,provider 可配)
L4 判定判定栈 hooks → Guardian → approval_policy;approval_policy 四档决定自动放行或弹出审批,审批框是应用窗口里的对话框
L5 工具shell(持久会话与一次性命令)、apply_patch、web_search、MCP server、code-mode(V8)
L6 记忆与存储rollout 会话记录、sqlite 状态库、memories、登录凭证(~/.codex)
配置面~/.codex/config.toml,企业 managed_config / MDM 优先级更高
沙箱read-only / workspace-write / danger-full-access 三档;macOS Seatbelt、Linux Landlock/seccomp、Windows 受限令牌;网络边界独立,经内置代理按域名放行

Claude Code 与此同构:settings.json 对应配置面,permissions 中的 allow/deny 规则是 L4 的判定输入,hooks 是配置面里的事件钩子[9]。开篇提到的 CVE-2025-59536,问题正出在 hooks 字段。

两条结构性事实

结构拆解完成,得到两条结论:这两条都源于结构本身的工作方式,不是实现上的 bug,后续推导的攻击面全部从这两条出发。

结构性事实 1:所有外部内容在组装时汇成同一条平坦的 token 流,来源信息在这一步丢失。

L2 组装请求时,系统提示、工具定义、对话历史、工具返回、读入的文件全部拼成一条序列,序列里没有结构化的来源标记。模型收到时无法区分哪段是产品开发者写的系统提示,哪段是攻击者在 issue 里写的评论。产品可以在文本中加入“以下是工具返回的内容”这类说明,但那是文本层面的约定,靠模型学习得来,不是结构保证。

Simon Willison 对同一事实的表述:“LLMs are unable to reliably distinguish the importance of instructions based on where they came from. Everything eventually gets glued together into a sequence of tokens and fed to the model."[7]

Codex 的 AGENTS.md 是一个直接例子:项目里的指令文件,内容直接进入上下文。克隆一个仓库,里面藏着什么指令,模型就收到什么指令,且无从分辨这些指令来自谁。

结构性事实 2:判定检查点位于“模型 → 动作”这一段,内容进入与配置加载都不经过它。

对照总图:L4 位于 L3 与 L5 之间,只管辖模型发起的工具调用。内容进入上下文走的是 L5 的读取工具或更靠前的通道,配置加载走文件通道直达配置面,两条路径都不经过 L4。

这一条是理解配置类漏洞的关键,CVE-2025-59536 的核心也在这里:hooks 配置在信任对话框弹出之前执行,用户还没来得及表达“我信任这个目录”,配置里的命令已经跑完。问题出在这条路径根本不在判定的管辖范围内,判定的规则本身没有写错。

判定与沙箱:两个独立的维度,而非二选一

这里需要纠正一个常见误解:把权限判定与沙箱当成两个并列、可以互相替代的免确认开关,即“要么弹窗问我,要么关进沙箱,二选一”,这个理解不成立,也正是不少产品缺陷的来源。判定与沙箱是两个独立的维度:

判定沙箱
回答什么问题这个动作允不允许发生动作发生时能碰到什么
机制性质逐动作、决策那一刻的一次检查执行全程持续的边界
强项灵活、细粒度、能表达意图确定、不依赖判断质量、内核级
弱项词法解析出错、审批疲劳、规则本身来自可写配置粒度粗、覆盖不全

判定的弱项恰是沙箱的强项,反之亦然。二者的正确关系是叠加,而不是二选一:沙箱兜底,判定只管辖要越出边界的动作。一道免确认的授权应当由边界覆盖来支撑,而不是由判定来支撑:判定放行只是“这次不问了”,这次执行至少要被沙箱关住;判定放行而执行落在沙箱约束之外,等于没有任何约束。

还有更糟的形态:本该兜底的沙箱反过来成为免确认的理由,即“反正有沙箱,别问了”。此时沙箱非但没有兜底,还为判定背书。这一形态留到沙箱一篇展开。

小结

后续展开的攻击面,没有一个以模型为攻击对象,全部落在包裹模型的代码上:配置、判定、沙箱与框架本身。这一模式会在系列中反复出现,也是许多“用 AI 防 AI”方案没有说清的前提。

授权机制的正确形态在这里先不展开,留到最后一篇绕完攻击面与攻击路径之后回收全文时给出。

下一篇从两条结构性事实出发推导攻击面:每个攻击面在哪里、攻击者如何写入、现有关卡为什么拦不住。

References

[1] Check Point Research — RCE and API Token Exfiltration Through Claude Code Project Files (CVE-2025-59536):https://research.checkpoint.com/2026/rce-and-api-token-exfiltration-through-claude-code-project-files-cve-2025-59536/

[2] NVD — CVE-2025-61260 (OpenAI Codex CLI ≤ 0.23.0):https://nvd.nist.gov/vuln/detail/CVE-2025-61260

[3] BleepingComputer — Researchers escape OpenAI Codex sandbox to run commands on host (Heapjack / Overpatch):https://www.bleepingcomputer.com/news/security/researchers-escape-openai-codex-sandbox-to-run-commands-on-host/

[4] Cato AI Labs — DuneSlide: Two Critical RCE Vulnerabilities (CVE-2026-50548 / 50549):https://www.catonetworks.com/blog/duneslide-two-critical-rce-vulnerabilities/

[5] NVD — CVE-2026-12537 (Google Gemini CLI):https://nvd.nist.gov/vuln/detail/CVE-2026-12537

[6] Embrace The Red — GitHub Copilot: Remote Code Execution via Prompt Injection (CVE-2025-53773):https://embracethered.com/blog/posts/2025/github-copilot-remote-code-execution-via-prompt-injection/

[7] Simon Willison — The lethal trifecta for AI agents:https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/

[8] OpenAI Codex 文档(config、sandbox、approval policy、AGENTS.md):https://developers.openai.com/codex/

[9] Claude Code 文档(settings、permissions、hooks):https://code.claude.com/docs/

[10] GitHub — openai/codex 源码仓库(codex-rs Rust 工程):https://github.com/openai/codex