一个 Git 配置键在七个编程 Agent 中执行了代码。2022 年的修复方案拦不住它

grith.ai 2026-10-29T03:55:56.828793

在交付的 .git/config 里写入一行恶意配置,在 git 2.43.0 上用三种方式运行。原生 git status:payload 触发。启用 2022 年的 safe.bareRepository 缓解措施:payload 仍然触发。清空 core.fsmonitor(Claude Code 每次调用都会这么做):被拦住。三种配置里只有一种挡住了攻击,而且它不是大家普遍引用的那个修复。

同一段 payload,三种配置。被广泛引用的 2022 年缓解措施,并没有覆盖通往 AI 编程 Agent 的那条投递路径。

2026 年 9 月 1 日,Manifold Security 的 Francisco Rosales 公布了 GitSpawn:在七个 AI 编程 Agent 上发现的八条代码执行路径,全部基于同一个原语。¹ 受影响的产品包括 Claude Code、Codex、Qwen Code、Goose、Grok Build 和 Hermes。研究发布时,八条路径中仍有四条尚未修复。

这个原语是一个叫 core.fsmonitor 的 Git 配置键。它的值是一条 shell 命令。Git 在常规的索引刷新过程中会执行它,也就是在 git status、git diff,或者 Agent 为了弄清楚仓库当前状态而执行的十几种操作之中。这条命令以开发者的身份运行,父进程是 git,而不是 Agent。

往下看之前,先说明一个决定这事跟你有没有关系的限制条件。光克隆一个恶意仓库是不够的。 Rosales 说得很明确:「克隆一个恶意 URL 不会有任何作用,fetch 和 pull 也一样。仓库必须以文件的形式到来,而且里面已经带着 .git 目录。所以攻击面是所有搬运目录而不是克隆目录的手段:共享的 .zip 压缩包、共享网盘、同步文件夹、U 盘。」¹

这是个实打实的限制,它把攻击范围缩小了不少。但它并没有消除风险。开发者无时无刻不在收到别人发来的目录,而且越来越多的人把这些目录直接交给 Agent,然后就走开了。

不新鲜的那部分

GitSpawn 有意思的地方,不是这个原语能用。而是这个原语能用这件事,我们已经知道四年了。

Justin Steven 在 2022 年发布过一份安全公告,公开了 core.fsmonitor 的滥用手法,针对的是“藏起来的裸仓库”。他讲清了攻击原理,演示了任意代码执行,并点明了与 Rosales 在 2026 年重申的相同交付限制:“这类问题无法通过 git clone 克隆的仓库来利用,因为克隆仓库时,远程端无法充分控制客户端 .git/ 目录里的文件。”

他也记下了 git 官方的立场,这一立场至今未变:这些是特性,不是漏洞,“git 目前没有计划改变针对仓库 .git/config 中潜在危险配置指令的现有行为。”

随后,2022 年 10 月 3 日,git 2.38.0 发布了一项缓解措施。把 safe.bareRepository 设为 explicit,git 就会拒绝操作它在工作树内发现的裸仓库。Steven 的公告用粗体告诉读者,强烈建议考虑设置它——这也是大多数评论文章会提到的配置项。

它需要用户主动开启,四年后的今天依然如此。而且,对于影响 AI 编码代理的攻击路径来说,它是错误的修复方案。这一点值得演示,而不是空口断言。

三种配置,同一个载荷

我搭建了尽可能最小、也最贴近真实的复现环境:一个自带 .git 目录的文件夹,正是 Rosales 描述的那种交付方式,里面携带了 core.fsmonitor = touch /tmp/fsmonitor-poc-fired; echo ""。然后在 git 2.43.0 上以三种方式运行 git status,检查标记文件是否被触发。

$ git status --porcelain                             # stock git
  PAYLOAD FIRED

$ git -c safe.bareRepository=explicit status --porcelain
  PAYLOAD FIRED                                      # the 2022 mitigation

$ git -c core.fsmonitor= status --porcelain
  BLOCKED

2022 年的缓解措施没能拦住它。这不是缓解措施本身的缺陷。safe.bareRepository 防的是埋在工作树里的裸仓库,那只是这类攻击的一种交付路径。对于一个完全普通的仓库、只是自带了恶意配置的情况,它无能为力——而当打开目录的是一个代理时,这才是真正关键的路径。

为求严谨,我确认了这项缓解措施在应当生效的地方确实有效。把 payload 放进一个深藏的裸仓库后,git 会直接拒绝:

fatal: cannot use bare repository '/tmp/.../buried.git'
       (safe.bareRepository is 'explicit')

两个变种、两道防御,而那个被广泛引用的设置,覆盖的恰好不是这里的问题。

一台工作机上实际配置了什么

2026 年 9 月 9 日,我检查了自己的开发机。系统是 Ubuntu,git 2.43.0,每天在用 Claude Code 和 Codex。

$ git --version
git version 2.43.0

$ git config --system --get safe.bareRepository; \
  git config --global --get safe.bareRepository; \
  git config --local  --get safe.bareRepository
(nothing)

三个作用域全都没有设置。这是默认状态,不是配置错误。Debian 和 Ubuntu 发布的 git 都不带这个设置,正常安装流程也不会把它打开。

于是问题变成:既然开发者这边的 git 显然不会防御,那 agent 会不会自己防御?每个工具大约三十秒就能验证,而上面测试输出的第三行才是关键——在调用时把 core.fsmonitor 清空,才是真正管用的做法。

工具 版本 safe.bareRepository core.fsmonitor
Claude Code 2.1.263, 2.1.265, 2.1.266 存在,设为 explicit 存在,已清空
Codex CLI 0.153.4 不存在 不存在
git 默认 2.43.0 未设置 从仓库配置中读取

任何人都能复现这个结果。用 strings 扫一遍 agent 的二进制文件,grep 这两个键,再和 git config --get 的输出对比。Claude Code 里带着字面字符串 safe.bareRepository=explicit 和 core.fsmonitor=,还有 -c core.hooksPath=/dev/null。这是一个很明显的模式:它用 -c 覆盖参数加固自己发起的 git 调用,而不是信任环境里的配置。

Codex 里这两个键一个都没出现——但要是因此断定 Codex 没有防护,那就错了,而且错得很容易。Codex CLI 确实受影响,OpenAI 已经修好。CVE-2026-19592 覆盖 0.102.0 到 0.130.0,在 0.131.0 中修复,macOS 和 Windows 上的 Codex Desktop 也有配套公告。OpenAI 自己的说明把这类问题为什么麻烦讲得很清楚:“helper 在 Codex 的命令沙箱之外运行,也不弹用户审批提示,攻击者控制的代码因此能以用户权限执行。”

所以两家厂商在严重性上并无分歧,分歧在于修复思路的层面。OpenAI 是直接堵死这条路径。Anthropic 按我能测到的版本,是在每次调用 git 时把那个危险的键置空——我的第三项测试表明,真正挡住这个攻击向量的正是这一步。它同时还带着 safe.bareRepository=explicit,把“埋藏”变体也一并覆盖了。

这才是正确的组合,而据我所知,没人公开这么说过。公告里写的是版本号,评论里说的是 2022 年那个设置,两边都没提到那行只需一个开关的调用参数——开发者,或者任何会调用 git 的其他工具,今天下午就能用上。

这张表有一个必须说明的局限。我只能检查自己磁盘上存在的版本,最早的是 2.1.263。Rosales 报告第二条 Claude Code 攻击路径在 2.1.252 上可用,所以缓解措施应该是在中间这十一个版本里的某个版本落地的。我没法确定具体是哪一个,也不打算声称 Anthropic 是针对这份报告才发布的。

披露记录

厂商的回应,才是这件事不再只是一个 git 故事的地方。

智能体 报告时间 厂商回应 修复状态 被 grith 拦截
Claude Code(core.fsmonitor) 2026 年 6 月 26 日 与同日一份报告重复 已在 2.1.196 中修复 是
Codex CLI 2026 年 9 月 1 日披露 OpenAI 发布了三个 CVE 已在 0.131.0 中修复,CVE-2026-19592 是
Qwen Code 2026 年 7 月 7 日 阿里 SRC 已受理 0.22.3 版本仍未修复 是
Goose 2026 年 7 月 13 日 已确认 已在 1.44.0 中修复,CVE-2026-72718 是
Grok Build 2026 年 7 月 14 日 以与早前报告重复为由关闭 1.0.13 版本仍未修复 是
Claude Code(ultrareview) 2026 年 7 月 15 日 与内部工单重复 2.1.252 版本仍未修复 是
Hermes 2026 年 7 月 20 日 未做分类处理 0.21.0 版本仍未修复,CVE-2026-71963 是

来源:Manifold 公布的表格,以及 The Hacker News 转述的 OpenAI 安全公告。最后一列每一行都一样,这是有意的。它并不代表厂商的什么属性,背后的复现过程放在本文末尾。

表里三行写着「重复」。这个词承担了太多含义。一份被以「重复」为由关闭的报告,意味着它被读过了、和某个已知问题对上了号,然后就没有下文了。Grok Build 的那份被以「与早前报告重复」关闭,之后一直没修。Claude Code 的第二条路径在 7 月被以「与内部工单重复」关闭,而 Rosales 在 9 月仍然能利用它。

Hermes 根本没走分类流程,它的 CVE 是在没有厂商参与的情况下分配的。

Anthropic 和 OpenAI 在这件事上都表现得不错,这一点值得直说。Anthropic 从收到报告到修好第一条路径只用了三天,而按我能测到的版本,它现在每次调用 git 都会把这个危险配置键清空——我的第三项测试表明,这才是真正堵住这个攻击途径的做法——此外还设置了 safe.bareRepository=explicit,把“埋雷”那种变体也一并覆盖了。OpenAI 堵住了这条路径,在三个产品上都推送了修复并附带了 CVE 编号,还坦率说明了自家的沙箱为什么没起作用。所以两家厂商在严重性判断上并没有分歧,分歧在于修复要修到哪一层。这才是正面案例该有的样子,也让上面那三行“重复”记录更难被原谅,而不是更好被原谅。

这才是正确的组合,而据我所知,还没有人公开这样说过。安全公告写的是版本号,评论文章写的是 2022 年那个配置,两边都没提到那个只需一行就能加上、开发者或其他任何调用 git 的工具今天下午就能采用的启动参数。

关于那张表,有一个需要如实说明的局限:我只能检查自己磁盘上存在的版本,最老的是 2.1.263。Rosales 报告第二条 Claude Code 路径在 2.1.252 上还能复现,所以修复应该落在中间这十一个版本里的某处。我无法确定具体是哪一个,也不打算声称 Anthropic 是针对这份报告才推送的。

为什么现有防护措施拦不住

审批提示。 智能体会在识别到风险命令时先询问用户。它执行的是 git status,这本身毫无风险,而恶意载荷并不在命令里。

沙箱和允许列表。 载荷并不是以智能体身份执行的,而是作为 git 的子进程执行,而 git 是智能体正常调用的。针对智能体自身进程的防护看不到它;而把 git 放进允许列表的防护,恰好把投递工具也放进了白名单。

仓库信任提示。 VS Code 在 2021 年就吃过这个亏,现在运行 git 前要求工作区已被信任。2 终端里的智能体没有对应机制,从头到尾就没有任何时刻问过用户:这个目录是否可信。

扫描器。 这里没有恶意软件。只有一个配置文件,里面有一个合法的键和一个字符串值——配置文件本来就该长这样。

谁能看见它

上面所有控制手段都位于代理之上,或与代理同级。下面这个则位于代理之下。

grith 在操作系统层面监管代理。它在代理的整个进程树上安装 seccomp-BPF 预过滤器和 ptrace 监督器,因此每个后代进程都会继承这份监管:代理本身、它启动的 shell、shell 运行的 git,以及 git 根据 core.fsmonitor 执行的那条命令。这个载荷之所以对代理不可见——因为它是以孙进程而非代理的身份执行——恰恰也是它无法逃过 grith 的原因。没有任何东西能绕过系统调用。

我在一个运行中的 grith 会话里复现了这次攻击,并读取了审计日志。6 被投放的仓库带有 core.fsmonitor = cat ~/.ssh/<key> > exfil,这是对真实载荷窃取凭据行为的替代品,指向一个诱饵密钥。仅仅运行一次 git status 就触发了它。grith 完全独立地给载荷发起的四次系统调用打了分,触发 git status 的代理对此毫不知情:

载荷尝试的操作 grith 的判定
在 ~/.ssh 下创建文件 排队——冻结等待审查
对该文件执行 chmod 拒绝——返回 EPERM
读取私钥 排队——冻结等待审查
事后删除证据 拒绝——返回 EPERM

两次修改操作被直接拒绝,系统调用返回值中注入了 EPERM,没有任何提示。对 ~/.ssh 的读取和写入则被排队,在 grith 中这意味着进程会冻结,在系统调用继续之前会询问人工,并明确说明操作内容:一个 git status 想要读取你的 SSH 私钥。在这次运行中,我故意批准了读取,以确认字节确实会流出,且判定是真实生效而非做样子。这次测试的重点是判定结果,而不是拦截。在非交互式或 CI 会话中,排队操作会默认拒绝,而不是等待一个永远不会到来的回答。

这些结果和是哪个 Agent 调用的 git 无关。grith 位于进程树之下,而不是藏在某个工具内部;所以无论 git 调用来自 Claude Code、Codex,还是上表里的任何一行,结论都一致。这也解释了披露记录里为什么最后一列始终不变,而厂商那一列能从「三天修复」一路排到「从未处理」。

这道缝隙落在哪里

从 GitSpawn 里真正值得记下的,不是七家厂商发了同一个 bug,而是一个四年前的老问题,被一个并不掌握四年前上下文的行业重新发现,然后拿四年前的缓解措施做了部分回应——而这个措施覆盖不到新的投递路径。

Rosales 没有引用 Steven 的公告,也完全没提 safe.bareRepository。我读过的报道里同样没有。这不是在批评这项研究——它做得仔细、文档齐全、披露也合规。只是说明一个事实:安全知识和工具是在两个不同的房间里长大的,而这类问题总是出在两者的接缝处。

底下有个更结构性的问题,和 MCP 工具投毒、Agent 技能注册表里出现的是同一个:不可信输入到达某个工具,工具照着它行动,而最终真正执行的东西,离任何人以为需要评估的位置还差着好几个进程。一个会调用外部命令的 Agent,会继承它调用的每个东西的全部风险;而它自己的权限模型只到自己进程边界为止。

这就引出一个让人不太舒服的问法:如果 Agent 的工作本身就是运行别的程序,那「给 Agent 做沙箱」到底指什么?不管答案是什么,它都不可能是围着 Agent 砌一堵墙——因为 Agent 每次调用外部命令,都把自己的权限直接穿过这堵墙带出去。沙箱必须位于 Agent 启动的一切东西之下,卡在载荷无法再往下钻的那条边界上。这正是 grith 在系统调用层拦截、而不在工具内部拦截的全部原因。

脚注

  1. Francisco Rosales,Manifold Security,《GitSpawn:单个漏洞即可让不可信仓库在 Claude Code、Codex、Cursor 和 Grok 中执行代码》,2026 年 9 月 1 日。https://www.manifold.security/blog/ai-coding-agents-git-hijack

  2. Justin Steven,《Git 隐藏裸仓库与 fsmonitor:各种滥用方式》,2022 年。https://github.com/justinsteven/advisories/blob/main/2022_git_buried_bare_repos_and_fsmonitor_various_abuses.md

  3. Git 2.38.0 版本发布,2022 年 10 月 3 日,引入 safe.bareRepository。https://git-scm.com/docs/git-config#Documentation/git-config.txt-safebareRepository

  4. 复现运行于 git 2.43.0、Ubuntu,2026 年 9 月 9 日。载荷为一个无害的 touch 标记,置于临时目录中,无网络访问。

  5. The Hacker News,《恶意 Git 配置可让 Claude 及其他 AI 编程代理运行攻击者代码》,2026 年 9 月 2 日。https://thehackernews.com/2026/09/malicious-git-configs-can-make-claude.html

  6. grith 复现,2026 年 9 月 9 日,在一台由 grith 监管运行 Claude Code 的机器上。core.fsmonitor 载荷读取的是为测试放置的诱饵私钥,而非真实凭据,并写入本地标记,无网络外连。判定结果取自 grith 自身的审计日志。该防护取决于 grith 在何处实施,而非由哪个代理调用 git,因此尽管只在一个代理下复现,仍被视为对披露记录中每一行均成立。

查看原文