在一套智能体系统里,会话 ID 是最无辜的那个字符串——不透明、没有语义,属于你看都不会看一眼就直接转交出去的那类值。而在 0.2.121 之前的 Python 版 Claude Agent SDK 里,你可以转交一个以连字符开头的会话 ID,Claude CLI 会把它读成一个参数。值得被摸到的那个参数是 --mcp-config,而这正是它拿到 9.2 分、而不是沦为一个脚注的原因:它不是执行一条命令然后收场,它挂上了一件模型可以反复调用的工具,而且挂的位置,在那套本该批准工具使用的权限系统的上游。
概览
一条记录,于 2026 年 11 月 1 日读自 GitHub 的公告数据库。
| 字段 | 取值 | 为何重要 |
|---|---|---|
| 标识符 | CVE-2026-96620 / GHSA-h4mw-j7qp-8mwm | 2026 年 9 月 12 日发布;2026 年 10 月 16 日复核并更新 |
| 软件包 | claude-agent-sdk(PyPI,Python) | 是那个 Python SDK,不是 CLI 本身 |
| 受影响 / 已修复 | < 0.2.121 / 0.2.121 | 是一次版本升级,不是一次配置调整 |
| 严重程度 | 危急——CVSS v4.0 基础分 9.2 | AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H |
| 弱点编号 | CWE-20、CWE-78、CWE-88 | 输入校验、操作系统命令注入、参数注入——按这个顺序 |
一条记录挂三个 CWE 并不常见,而这是这里信息量最大的一栏。数据库之所以挑不出一个,是因为这个 bug 是一条序列:一个值没有被校验(CWE-20),于是它得以被当作参数而不是数据来读(CWE-88),于是它得以抵达一条命令(CWE-78)。修掉第一环是那个补丁。而注意到第二环根本就存在,才是那个可以推广的部分。
到底发生了什么
公告自己的描述只有三句话,而每一个分句都承重。SDK「把 resume 选项未经校验地传给了 Claude CLI」。一个以 - 开头的值可能被读成一个独立的 CLI 参数。控制了该值的攻击者可以注入 --mcp-config 来定义一台 stdio MCP 服务器,而它会在 CLI 启动时运行任意命令——并且,用公告的原话,这「绕过了 SDK 的权限控制」。
利用它需要应用方配合一件事:把不受信的输入当作 resume 的会话 ID 传进去。这不是什么离奇的要求。只要你做的东西里带对话,恢复会话就是必经之路,而会话句柄天然就是你会塞进 URL 路径、查询参数、webhook 载荷或任务记录里的那个东西。它是个句柄,所以你就按句柄对待它:不解析、不解释,原样递回给当初签发它的那一方。所有让会话 ID 用起来省心的直觉,恰好就是让它被不加审视地转交出去的那个直觉。
向量里的 AT:P——存在攻击前提——就是 SDK 的防线,而且仅此而已。部署里必须有某个环节把受攻击者影响的文本接到那个参数上。一旦这件事成真,SDK 里没有任何东西阻止它被利用;而 SDK 也没有任何地方告诉过你,那个参数是危险的。
关键在那个参数
参数注入是个老旧的漏洞类别,对它的常规解读是「攻击者让一条命令跑起来了」。这种解读把这一例说轻了,因为智能体外壳上可用的那些参数并不是普通的参数。一副外壳的命令行,是你告诉它用哪个模型、在哪个目录干活、触发哪些钩子、不问就可以做什么、以及挂载哪些工具服务器的地方。它是这副外壳的配置通道,也就是说,它是控制平面。一个被注入的参数不是一条被执行的命令;它是一次对策略的改写。
--mcp-config 是那个面上最糟糕的一项,而为什么糟糕值得说准。一台 stdio MCP 服务器是由一条用于启动它的命令来定义的。所以定义一台,就拿到了启动时的执行——这是 CWE-78 的那一半,而它本身已经够危急了。但它还拿到了某种活得比那次启动更久的东西:会话工具清单里的一台服务器,对外宣告着若干工具,供模型在整个会话期间调用。这次注入产出的不是一个事件,而是一项能力。
这也正是权限系统为何并不挡在路上。SDK 的那套控制决定的是:模型提出的某次工具调用是否可以放行。它是一条路上的一道闸门。被注入的参数走的是配置那条路,而且它在模型还没提出任何东西之前就已抵达——所以它并没有击破那道闸门,它是把闸门被要求去比对的那份东西给填好了。一道只决定「清单上哪件工具可以运行」的闸门,对「这份清单是谁写的」无话可说。这与环境权限是同一种结构性失效:权限早就在进程里了,而检查站在它的下游。
这事可以推广到哪里
把你自己的集成过一遍,列出你的应用递给智能体外壳的、每一个源自外部的值。会话或线程 ID。模型名称,当它来自某项用户偏好时。工作目录,当它来自某条项目记录时。一个提示词文件路径。一个 profile 或配置名。这些每一个都是你之所以会转交、正因为它「不是内容」的字符串,而转交它本身就是目的。对每一个要问的问题不是「它会不会含有 shell 元字符」——那是 2005 年的问题,而沙箱基本已经回答了它。该问的问题是:它会不会抵达 argv,而住在那儿的最强参数是哪一个。
有两种做法修的是这一类,而不是这一例。用上参数终止符:-- 之后的一切都是位置参数,所以一个不可能成为参数的值就无法变成参数;仅此一处改动,就会让一个以连字符开头的会话 ID 变成一个无效的会话 ID,而不是一次配置改写。更好的做法,是把不受信的值彻底挡在 argv 之外——走 stdin、走一个由你自己设置的环境变量、或走一个路径由你掌控的文件——并把命令行留给你自己的代码选定的那些值。0.2.121 里的补丁校验了 resume 的值,这是正确的,也更窄:它关掉的是被报上来的那一个参数。
然后请把你目前正一并处理的那两场评审分开。「这次工具调用可以放行吗」,是你的权限系统回答的问题。「这个会话里存在哪些工具,以及是谁定下来的」,是另一个问题,它在启动时就被回答了,而在大多数外壳里没有任何东西记录下那个答案。爆炸半径是由后者决定的。
那两个日期
这条记录带着两个日期:2026 年 9 月 12 日发布,2026 年 10 月 16 日复核并更新。你是在哪一天听说它的,完全取决于你的工具链对准的是哪一个,而两者相隔三十四天。如果你是在十月下半月从依赖扫描器那里得知这件事,那么这条公告当时已经存在了一个月;如果你读的是发布说明,那你九月就拿到了。这不是在批评任何人——整编需要时间,而正因为数据被整编过,扫描器才好用——但在你断定自己是「最新的」之前,值得先知道自己对的是哪一口钟。
实操检查只要一分钟:pip show claude-agent-sdk,若版本低于 0.2.121,就升级。然后在自己的代码里 grep 一下 resume 或会话参数是从哪里来的,因为升级关掉的是那个参数,并不改变这样一个事实:你一直在把攻击者可触达的文本,转交进一副外壳的命令行里。
常见问题
CVE-2026-96620 是什么?
是 Python 包 claude-agent-sdk 在 0.2.121 版本之前的一个参数注入缺陷。SDK 把自己的 resume 选项未经校验地传给了 Claude CLI,于是一个以连字符开头的值可能被解释成一个命令行参数。GitHub 以 GHSA-h4mw-j7qp-8mwm 追踪它,CVSS v4.0 基础分 9.2。
我受影响吗?
只有当你用的 Python SDK 版本低于 0.2.121,并且你的应用把你并不掌控的输入当作 resume 的会话 ID 传进去时,才会受影响。公告明确写出了这个前提,而 CVSS 向量把它记作「存在攻击前提」。不论如何,升级都会消除这份暴露。
既然需要应用方配合,为什么分数还这么高?
因为那个可被摸到的参数能干什么。注入 --mcp-config 会定义一台 stdio MCP 服务器,它在 CLI 启动时运行一条命令,然后作为一件模型可以调用的工具留在会话里。机密性、完整性与可用性三项影响指标都是高,而公告指出它绕过了 SDK 的权限控制。
这是否说明 MCP 不安全?
不。MCP 服务器本来就应该由运行该智能体的人来配置,这正是它的功能。缺陷在于一个来自外部的值抵达了配置通道。任何能挂载工具的参数,交到攻击者手上都同样危险;问题出在这个值走过的路径,而不在路径尽头的那份协议。
沙箱能把它圈住吗?
能圈住一部分,而且不是你希望的那一部分。沙箱限制了被注入的那条启动命令能碰到什么,这是实打实的缓解。但对于那台被挂上的服务器在接下来整个会话里都留在工具清单上这件事,沙箱无能为力,因为那恰恰是智能体在严格照自己的配置办事。
对我自己的代码,一句话的修法是什么?
别把不受信的字符串放进 argv。走 stdin、走环境变量、或走一个你掌控的文件;非得用命令行时,把值放在 -- 之后,让它无法被读成参数。
延伸阅读
本站内容:
- 外壳配置作为注入目标——一般情形,以及那个面上的其他参数。
- 环境权限——为什么下游的检查修不了上游的授权。
- 自动批准与命令允许清单——从另一个方向看「argv 还是字符串」这一课。
- 生命周期钩子与外壳配置——把配置面当成一个运维问题。
- 爆炸半径——一件被挂上的工具,如何改变一次失误的尺寸。