安全攻防
morning
【AI安全】Agent 写文件时,路径检查通过后为什么还会越界?
2026-11-11
1 阅读
约10分钟阅读
先知社区
字号:
引言 以前看 Agent 文件写入,通常先检查一件事:目标路径有没有留在 workspace 里。Claude Code 公开的 CVE-2026-103435 让我开始重新看这个问题。公告描述了检查与写入之间的时间差:权限检查通过时目标还在项目目录内;共享工作区中的另一个进程替换目录项后,Agent 再按原路径写入,文件就可能落到项目目录之外。 Anthropic 的公告 我想弄清楚的是,检查通过时程序确认的究竟是什么。如果它确认的只是一个路径字符串,那么等到真正打开文件时,内核解析到的还会是同一个对象吗?即使函数最后返回拒绝,拒绝之前有没有可能已经改动磁盘?沿着这两个问题,从 Claude Code 公告中的时序开始,再去看 OpenClaw 和 OpenShell 的真实写入与同步函数。 一、路径检查通过后,目标仍可能变化 1.1 从 Agent 工具到文件系统 Agent 发起一次 write_file(path, content) ,宿主侧还要解析参数、检查权限、打开或创建文件,再把内容提交到磁盘。模型给出目标路径和数据,最终调用文件系统接口的是持有宿主权限的进程。 权限检查处理的是某一时刻的路径;写入操作针对的是最后实际打开的文件对象。如果实现只把路径字符串传给后续的 open() 、 rename() 或同步函数,文件系统就会再次查找目录项。同一字符串因此可能先后指向不同对象。 这里讨论的是本地文件系统中的共享目录项竞态。攻击者需要能替换相关目录项,并赶在 Agent 后续使用路径前完成操作;宿主进程则需要有权访问工作区外目标。攻击者借用的是宿主已有的文件权限。若这两个条件不成立,越界链就无法完成。 1.2 路径名、目录项与 inode workspace/draft.txt 是一次查找所用的路径名,不是文件本身。文件系统会从起点逐级查找目录项: workspace 目录项指向一个目录对象,目录中的 draft.txt 再指向具体文件对象。对本文讨论的本地文件系统,可以用 inode 标识文件对象;路径名描述“怎么找到它”,inode 描述“这次找到的是谁”。 符号链接本身保存一段路径,解析器遇到它时会继续查找链接目标。若相关目录项在两次操作之间被替换,同一条路径字符串下一次解析时就可能指向另一个对象。硬链接则会新建一个指向同一 inode 的目录项,不会把一个路径重定向到另一个 inode;但通过任一硬链接修改文件内容,其他名字看到的也是同一份内容。前者改变路径解析结果,后者让多个名称共享同一个文件对象,两类风险不能混为一谈。 文件描述符则绑定到已经打开的对象。打开后,即使原路径名被重命名或替换,已有描述符仍引用原先打开的文件;程序若随后再次调用 open(path) 、按路径执行 rename() ,或让同步函数重新读取 path ,仍会进行新的路径解析。也就是说,FD 可以固定一次打开的对象,但只有在安全地完成打开之后才有这个作用;如果第一次打开已经落到越界目标,FD 只会稳定地指向这个目标。 机制的初始目录如下: 在这个最小目录里, workspace/draft.txt 初始解析到工作区内的 inode A, outside/canary.txt 是工作区外用于观察状态变化的 inode B。若 workspace/draft.txt 的目录项被替换成指向 outside/canary.txt 的符号链接,这串路径字符可以保持不变,下一次查找却会走到 inode B。父目录重绑定也是同样的道理:后续查找会从被替换的目录项继续解析。最终要靠 Canary 读回确认受影响的对象,不能只根据函数返回值判断有没有越界。 1.3 打开与校验的先后顺序决定副作用 路径检查通过,不代表后续操作仍会作用于当时检查的对象。检查之后如果目录项被替换,程序再次按路径打开或提交文件,就可能走到另一个目标;如果 open() 本身带有截断或创建标志,副作用还可能早于最终校验。 真正的问题在于,同一条路径会被多次解析:第一次用于检查,之后又用于打开、提交或同步读取。目录项只要在两次解析之间发生变化,后一次操作就可能落到另一个对象;带有 O_TRUNC 或 O_CREAT 时,文件状态甚至会在拒绝返回前先改变。三个案例分别把这个窗口放在写入、打开和同步阶段。下面先从 Claude Code 的源码看路径为什么会被重新解析,再把相同的时序落到 OpenClaw 和 OpenShell 的实际文件状态上。 二、Claude Code:检查通过后,写入目标为何还会变化 2.1 公开问题与触发条件 Anthropic 披露的 CVE-2026-103435 描述了一次发生在正常文件编辑中的越界写入:Claude Code 检查目标路径时,文件还位于项目目录;真正写入时,程序再次按路径名解析目标。共享工作区里的另一个进程可以在这段间隙替换目录项,让原路径转而指向项目外的文件。 Anthropic 公告 将受影响版本列为 < 2.1.129 ,并在 2.1.129 修复。 要让这条链成立,攻击者必须能修改共享工作区中的目录项,并在文件写入前完成替换;宿主进程也必须有权限访问项目外的目标。 工具参数始终是 workspace/draft.txt ,变化的是这串路径对应的对象。接下来沿源码追踪权限检查结果是否被写入阶段继续使用。 2.2 从路径校验到文件写入的源码调用链 沿着 Claude Code 2.1.87 的源码快照追调用链。这里的源码只用来说明权限检查结果如何传到落盘函数,不把它写成 Claude Code CLI 的运行结果;问题背景和受影响版本仍以 Anthropic 公告为准。 FileWriteTool.checkPermissions() 将工具输入交给 checkWritePermissionForTool() ( FileWriteTool.ts,L135–L142 )。该函数通过 tool.getPath(input) 取出目标;如果调用方没有提供预先计算的路径集合,才调用 getPathsForPermissionCheck() 生成待检查路径。 源码位置: filesystem.ts,L1205–L1375 。 这里不是简单地检查路径里有没有 ../ 。 getPathsForPermissionCheck() 会把原始路径、符号链接链上的目标和最终解析路径放进待检查集合;目标尚不存在时,它会向上找到最深的现存路径,解析其中的符号链接,再把尚未创建的路径部分补回候选集合。具体实现位于 fsOperations.ts,L288–L381 。之后, pathInAllowedWorkingPath() 再把这些路径与当前工作目录及附加工作目录比较。 源码还要求 precomputedPathsToCheck 必须由同一个工具输入派生,避免把旧路径集合用于另一个请求;但这仍只是路径集合,不会固定文件对象。 检查函数返回的是 PermissionDecision ;它没有打开目标,也没有把一个已打开的文件描述符交给写入阶段。权限检查通过后, FileWriteTool.call() 又从
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱