前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9866
  • 用免费模型自动分类CI失败:四类故障一键兜底
  • IBM发布Granite 4.2主打本地部署与企业Agent
  • IBM 开源 Granite 4.2:3B/8B/30B 三规格,内置 Agent 能力
  • 团队AI数据库访问的审计与管控实践
  • Paul Dix:AI 写了 100 万行代码并持续自主修复
  • Ponytail:让 AI 代码助手复刻「沉默老鸟」思维
  • 用 LLM-as-a-Judge 在 CI/CD 中拦截 Prompt 回归
  • OpenRouter 一键调用全球大模型:注册到 WordPress 流式输出全流程
  • Linus Torvalds 用 AI 辅助调试:24 个补丁、18 次内核启动定位 Intel Xe 内存破坏
  • IBM Granite 4.2 发布:开源推理模型 + 原生 Agentic RL,8B/30B 可操控终端
  • awesome-llm_apps:100+ AI Agent 与 RAG 应用开源集合
  • 搜蚁智选 AIMALL MCP Server:统一接入多供应商 AI 工具
  • Apple/Google 推机密计算AI:隐私计算如何破解企业级AI数据孤岛
  • 2026年AI Agent记忆平台横评:Mem0/Zep/Letta谁更适合生产级多轮对话
  • 蚂蚁发布TST 2.0时序大模型登顶权威测评
  • 模型对比的单次跑分不可信:我用 5 次重跑验证了一次「险胜」
  • VS Code推出持久化Agent会话支持
  • 旅游公司用OpenAI Codex让非技术人员也能快速构建产品
  • Sentence Transformers多向量嵌入模型微调教程
  • Liquid AI 开源 Pipette:端侧模型性能基准测试平台
  • AI Agent写防护存在符号链接逃逸漏洞
  • Prompt缓存:RAG场景实测可降成本10倍
  • 2026企业LLM部署必查清单:数据隔离、身份认证与全链路加密
  • GitHub实战:生产前评估LLM的内部方法论
  • NeuronScope:命令行追踪LLM输出背后的神经元
  • GitHub Copilot Customize tab正式开放MCP集成
  • Shopify CEO威胁封杀Claude Code
  • 遥测管道:控制AI Agent生产成本的工程实践
  • Amazon OpenSearch MCP Apps实现Agent可观测性
  • AI Agent 评估:聚合分数为何掩盖单层故障
  • OpenAI 首枚推理芯片 Jalapeño 曝光,性能超越 Blackwell 与 Rubin
  • Claude Cowork 和 Chat 终于共享记忆了
  • Meta 开源 MetaRoCE:专为万卡级 AI 集群设计的 RDMA 传输协议
  • Anthropic 统一 Claude 记忆系统:Chat 与 Cowork 共享上下文
  • GitHub Copilot 升级为 AI Agent 控制中心
  • LLM Prompt Caching 隐藏的 90% 折扣:实测 393 个模型
  • Poka-Yoke技能包让Claude Code设计预判能力从42%升至81%
  • Qwen3.8-Flash-Next 开源即获 SGLang 原生支持
  • Z.AI开源GLM-5.3-Flash:原生视觉+高效稀疏注意力
  • 我在树莓派上跑 Qwen 做本地车载 AI
  • OpenAI发布Jalapeño推理芯片:每瓦性能超英伟达GB300达1.9倍
  • 阿里千问预告开源 Qwen3.8-Flash-Next:下一代 Qwen4 架构预览版
  • OpenAI Jalapeño芯片推理能效双突破
  • AI Agent记忆:相似性判断不能获得投票权
  • OpenAI Jalapeño 芯片:自研 ASIC 叫板英伟达 Blackwell
  • OpenAI 定制推理芯片 Jalapeño:九个月完成,代码由 AI 重写
  • AI编码助手放CLI还是IDE?先做好验证机制
  • Ruflo 平台认证前RCE漏洞:单个POST请求接管AI Agent
  • 苹果M5 Ultra发布:4芯片UltraFusion架构,80核GPU本地跑千亿模型
  • 开源Oblivion AI:Python实现的免费AI编程助手
  • AI生成代码通过单元测试却没过TSan:并发bug的警示
  • 已加载 51 / 9866
8.0
热点
AI SCORE
行业动态2026-08-26 07:04

AI Agent写防护存在符号链接逃逸漏洞

dev.to · AI#AI安全#Agent漏洞#路径穿越
Editor brief · 编辑速览

几乎所有路径检查防护都无法抵御符号链接穿越:allowed目录下创建一个指向目录外的symlink,即可将文件写出白名单范围。path.resolve和path.relative是字符串运算,不追踪真实文件系统路径。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

几乎每个写保护最终都会用到下面这个路径检查,包括我自己写的那个。它比明显的版本好一些,但仍然有问题。

import { isAbsolute, relative, resolve } from "node:path";

function contains(root, target) {
  const rel = relative(resolve(root), resolve(target));
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
}

它正确地拒绝了 ../../etc/passwd。当只允许写入 src 目录时,它也正确地拒绝了 src-secret/keys.json——而那个 naive 的 target.startsWith(root) 在这里会出错。如果你写过这个函数,大概测试过这两种情况,看到它们被拒绝,然后就继续往下走了。

现在,在允许的目录里放一个符号链接:

project/
  products/          <- 唯一允许 agent 写入的目录
    cache -> ../../secrets
secrets/
  notes.txt

然后问保护机制:project/products/cache/notes.txt 能不能写:

1. lexical guard  -> defer (approved)
2. write landed in C:\...\escape-UlmQ6t\secrets\notes.txt — outside products/

这是真实复现过程中的记录,第二行才是关键:文件被写入了,而且写到了 allow-list 之外。保护机制不是没有运行——它运行了,检查了路径,然后说可以。

path.resolve 和 path.relative 是字符串操作。它们会折叠 . 和 ..,规范化分隔符,但从来不真正打开任何东西。resolve 一个路径是对文本的一个声明,而不是对文件系统的声明。

所以 products/cache/notes.txt 规范化之后还是 products/cache/notes.txt,它位于 products 之下,而 products 是允许的。每一步都是正确的。错误在比较操作的上游:保护机制在比较两个名字,而它真正想约束的是两个位置——在有链接的文件系统上,这两者不是同一回事。

这不是经典的路径遍历漏洞。遍历漏洞是输入里包含 ..,上面的检查能抓住它。这里输入里没有任何可疑的东西——没有 ..,没有绝对路径,没有编码技巧,reviewer 不会在任何一个地方画圈。重定向发生在磁盘上,不在字符串里。

谁会在那里放链接

这件事值得说精确一点,因为"攻击者植入了一个符号链接"并不是常见情况——假装它是会让这个风险很容易被Dismiss掉。

项目目录里的链接是再普通不过的东西。node_modules 里全是链接,而 pnpm 的整个目录布局本身就是靠符号链接撑起来的。构建工具把缓存链接到指定位置。Monorepo 工具链把各个包互相链接起来。任何曾经把 data/ 目录指向更大磁盘的人都这么干过。如果你的 allow-list 是 products/,而它下面有链接,保护机制就有了一个洞——但这不是谁的错。

然后还有 agent 本身。一个无人值守的 agent 会运行 shell 命令,而 shell 命令里创建目录链接不是 Write 操作——钩子根本看不到它,因为匹配器匹配的是工具名称,而 Bash 不属于文件写入工具之一。一句 ln -s 就建立好了这条路由,之后所有经由它的写入都会被保护机制批准——而这个保护机制做的工作正是它被设计来做的事。

我不是说这让钩子变成了沙箱。它从来就不是——一个能跑 shell 的 agent 可以随意写到任何地方,根本不咨询你的钩子,这也是为什么要有 git-diff 检查。说法要更窄、更糟糕:保护机制在这里是沉默的。它把写入报为"在范围内",这和一切正常时的报告一模一样。

Windows 让它更容易,而不是更难

直觉会认为这是 Unix 问题,因为在 Windows 上创建符号链接需要管理员权限或开发者模式。对于符号链接来说确实如此——但无关紧要,因为链接不是 Windows 唯一支持的链接类型。

在关闭开发者模式的 Windows 10 上探测:

FAILED TO CREATE: EPERM  dir link (symlink)
FAILED TO CREATE: EPERM  file link (symlink)
defer                    dir link (junction)

两种符号链接类型都被拒绝了。目录 junction 没有任何提示、不需要提权,就被普通用户进程创建了出来——而这个 junction 才是打败保护机制的那个。fs.symlinkSync(target, path, "junction") 是 Node 里一行代码,mklink /J 是 shell 里一行命令。每个人记得的那个权限门,守的是那扇本来就锁好的门。

diff 也抓不到这个

对于"钩子看不到一切"的标准答案是:额外在提交前 diff 一下 git status,然后把改动的路径和同一个 allow-list 比一比。我用那个逃逸跑了一遍看能抓到多少:

git status --porcelain:
?? products/cache/

checkScope -> {"ok":true,"changed":["products/cache/"],"violations":[]}

什么都没抓到。Git 报告的路径是 products/cache/,在 allow-list 里面,所以 scope 检查通过了。两层保护机制都在用同样的方式比较同样的名字,所以它们共享同一个盲点——两个独立的检查以相同方式失败,就是一个检查穿了件马甲。

修复方案,以及它会引入的 bug

问问文件系统这个路径实际上指向哪里:

import { realpathSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";

export function resolveLinks(path, realpath = realpathSync) {
  let current = resolve(path);
  const tail = [];
  for (;;) {
    try {
      return tail.length ? join(realpath(current), ...tail) : realpath(current);
    } catch (err) {
      if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") return null;
      const parent = dirname(current);
      if (parent === current) return resolve(path);
      tail.unshift(basename(current));
      current = parent;
    }
  }
}

这个循环之所以需要,是因为 Write 的目标通常还不存在——这本来就是 Write 的意义——而对不存在的路径调用 realpath 会抛出 ENOENT。所以一直走到最近的存在着的祖先那里再解析,然后把跳过的段落重新接回去。这些段落不可能藏着链接,因为它们根本不存在。

现在说重要的部分,这是在第二次复现时才看到的。把解析后的目标喂给旧的比较逻辑,合法写入开始被拒绝:

5. resolving the target only -> deny on a legitimate write
   both sides resolved       -> defer

如果项目根目录本身也是通过链接到达的,目标解析到真实位置,而 allow-list 的根没有解析,它们就不再共享前缀,一切都被拒绝了。这不是稀罕情况:macOS 上 /tmp 就是 /private/tmp,/home 常常是挂载点后面藏着链接,CI 的 checkout 路径也经常是指向 workspace 目录的链接。两边都解析,或者都不解析。只解析一边比都不解析还糟糕,因为它在普通情况下就失败了,而不是在罕见情况下。

两个更小的决定,都值得主动做一下:

当 realpath 拒绝回答时,拒绝。符号链接循环给出 ELOOP;读不了的目录给出 EACCES。在钩子的其他地方,正确的失败策略是继续往下走——一个刚输错 allow-list 就拦住所有写入的保护机制,你到午饭时间就会把它关掉,而且钩子契约里每个失败路径最终都是"继续"。这一个案例是例外,原因是这不是保护机制的失败,而是文件系统拒绝说出某个路径指向哪里。一个未知位置不是可以批准的那个。

把解析后的路径放进拒绝消息里。有意义的拒绝是那些实际路径和请求路径不一致的情况,因为那个差异就是链接:

Write to C:\...\escape-UlmQ6t\project\products\cache\notes.txt is outside this
agent's write scope. It resolves to C:\...\escape-UlmQ6t\secrets\notes.txt.
Allowed: products. If this file genuinely needs changing, say so and stop — do
not work around the guard.

模型会读到这串字,你凌晨三点也会读到。

这仍然没修掉什么

检查发生在钩子里;写入发生在钩子返回之后。在这两个时刻之间,链接可以被创建或重新指向,而 PreToolUse 钩子没有任何办法关上那扇窗——这是 time-of-check-to-time-of-use 窗口,是通过名称检查路径这件事固有的问题。真正的边界是操作系统层面的:容器、看不到磁盘其他部分的用户账号、文件系统命名空间。如果你有的只是一个钩子,那你有的就是对失误和对 agent 从它读到的文件里捡到的指令的保护——它值得有,但它不是沙箱。

改变的是那个沉默。以前保护机制批准了写入并报为"在范围内"。现在它拒绝并说出路径实际上去了哪里。

这个 bug 能在 review 里活下来的原因是:测试套件只断言了保护机制允许正确的操作并拒绝明显错误的那几个。每个断言都通过了,有洞之前和之后都是,因为谁都不知道链接是一个类别。

所以:在临时目录里创建一个真正的链接,把写入指向它,然后断言"拒绝"。不是 mock 的 realpath——是真实文件系统上的链接,Windows 上是 junction,其他地方是 dir 链接,如果平台拒绝创建就打印一个 SKIP 跳过。而且在你信任新测试之前,故意把保护机制弄坏,看它会不会失败。一个从来没被看到失败过的检查不构成任何证据。

Originally published at fewparts.co.uk.

Agent Guardrails Kit 是这个代码的免费组装版本——同样的模块,连在一起,加上测试。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
Liquid AI 开源 Pipette:端侧模型性能基准测试平台
下一篇
Prompt缓存:RAG场景实测可降成本10倍