Vite 又出安全通告了,这次是 CVE-2025-32395,任意文件读取漏洞。说实话,dev server 相关的漏洞我见过不少,但这个有点不太一样:它不是靠一个明显不安全的接口翻车,而是栽在“URL 解码、路径规范化、目录白名单检查”这几层逻辑的先后顺序上。只要你用了受影响的 Vite 5.x / 6.x 版本,攻击者构造一段特殊编码的 URL,就能绕过server.fs.allow白名单,把工作目录之外的文件读走。这篇文章我按自己的复现过程整理出来:影响范围、绕过原理、两种公开 PoC 的写法、修复方案,以及我在排查时踩过的几个坑,看完可以直接照做。
1. 漏洞概述与整体分析
1.1 漏洞身份与影响范围
先给基本信息,方便你对号入座。
| 项目 | 内容 |
|---|---|
| CVE 编号 | CVE-2025-32395 |
| 漏洞类型 | 任意文件读取(Path Traversal / Access Control Bypass) |
| 受影响组件 | Vite dev server |
| 受影响版本 | Vite 5.x 系列、6.x 系列中低于官方修复版本的版本 |
| 官方修复版本 | 5.4.17、6.2.7 及更高版本(以官方公告为准) |
| 攻击条件 | 攻击者能访问 dev server 端口 |
漏洞出在 Vite 开发服务器的静态文件服务模块。Vite 在开发模式会提供一个本地 HTTP 服务,让浏览器能直接请求源码模块。为了保护工作目录之外的敏感文件,Vite 默认开启server.fs.strict,并通过server.fs.allow配置允许访问的目录白名单。CVE-2025-32395 就是绕过这个白名单的漏洞。
这里要特别强调一句:这不是应用代码写错了,而是工具链自身的逻辑缺陷。哪怕你的项目代码完全合规,只要用了受影响版本的 Vite,就等于默认暴露在风险里。所以这不是“写得不好的人才需要关心”的问题,而是所有 Vite 用户都该看一眼的问题。
1.2 为什么一个“开发服务器”的漏洞值得重视
很多人第一反应是:dev server 不是本地开发时才跑的吗?能有什么风险?
这个想法在几年前还站得住脚,现在不行了。前端开发早就不是“本地起个服务自己看”的时代了:
- 很多团队用远程开发机、云开发环境,dev server 要暴露在局域网甚至公网才能让团队访问。
- 不少内部工具、低代码平台直接把 Vite dev server 当成静态资源服务来用,长期挂着。
- 如果 dev server 监听在
0.0.0.0,同一局域网的其他机器都能访问。 - 即便只监听 localhost,浏览器恶意网页也可能通过 DNS rebinding 等方式请求到本机端口。
攻击者只要能访问到 dev server,就可以读取应用源码、配置文件、环境变量、密钥甚至系统文件。源码泄露本身就很麻烦,更别说顺藤摸瓜找到数据库口令、云服务密钥这些敏感信息。所以这个漏洞的实际危害,完全取决于 dev server 的暴露程度,而不是“本地服务”这个标签。
1.3 和 Next.js 对比:为什么 Vite 出问题影响面这么大
这段时间网上经常能看到 “nextjs 和 vite” 的对比讨论。两者定位不完全相同:Next.js 是完整 React 框架,自带路由、SSR 和打包体系,开发服务器用的是 webpack 或 Turbopack;Vite 更偏向构建工具和开发服务器,主打快速冷启动和 ESM 原生体验,被大量用于 SPA、组件库、桌面端、移动端的工程化脚手架。
问题在于,Vite 已经是新一代前端工具链的事实标准之一,npm 上的下载量和使用量非常大。一个 Vite 的 dev server 漏洞,影响的不是你一个人的项目,而是所有基于 Vite 搭建的前端工程,包括 Vue、React、Solid、Svelte 等各种生态的项目。这就是工具链漏洞和业务代码漏洞最不一样的地方:业务代码出问题只影响一个项目,工具链出问题影响的是整个依赖它的项目群。
2. 核心原理拆解:为什么 URL 编码能绕过白名单
2.1 dev server 是怎么“往外发文件”的
要理解这个漏洞,先要知道 Vite dev server 的文件服务机制。
开发模式下,浏览器请求的入口页面会通过<script type="module">加载源码模块。Vite 会把浏览器请求的路径映射到磁盘文件,做转换后返回。但这个映射不是无限开放的,Vite 内置了几层限制:
server.fs.allow:允许被访问的目录白名单,默认是 workspace 根目录(通常就是你项目的根目录)。server.fs.deny:黑名单,默认包含.env、.git、node_modules里的敏感文件等。server.fs.strict:严格模式,默认开启,禁止访问 workspace 根目录之外的文件。
其中allow的检查逻辑很关键。Vite 在判断一个文件是否允许被访问时,不是直接比较路径字符串,而是借助了 chokidar 的 glob 匹配能力,因为allow配置里支持通配符。比如你配置了allow: ['/home/user/project/**'],Vite 就需要用 glob 模式去匹配请求的文件路径是否落在这些目录下。
这里就埋下了一个隐患:glob 匹配对路径字符串的处理方式,和 Node.jsfs模块真正读取文件时对路径的处理方式,并不是完全一致的。
2.2 检查与读取的“错位”
我把这个漏洞的本质概括成一句话:检查时看到的路径,和读取时用的路径不一样。
打个比方,小区门卫检查访客的登记单,看的是复印件,上面写着“3 号楼 302”,门卫觉得没问题就放行了。结果访客进门之后,掏出的原件上写的其实是“隔壁小区 302”。门卫看的复印件和实际执行的原件对不上,人就溜进去了。
Vite 这次的问题类似。正常情况下,一个请求进入 dev server 后,会经历这样的流程:
- 解析 URL,解码
%2f这类编码字符。 - 去掉
/@fs/、/@id/这类特殊前缀。 - 拿到文件路径后,用
allow白名单做 glob 匹配检查。 - 检查通过后,用
fs.readFile读取文件内容并返回。
在正常路径下,第三步和第四步用的是同一个路径,所以逻辑成立。但攻击者可以通过特殊编码构造一个路径,让它在第三步“看起来安全”,在第四步“实际越权”。
CVE-2025-32395 的公开利用方式里,比较常见的是这么两个:
http://localhost:5173/@id/__x00__/etc/passwdhttp://localhost:5173/@fs/%2f%2fetc%2fpasswd
第一条里的__x00__是 Vite 内部使用的一个占位符,代表 NUL 字符(\x00)。这个字符不会出现在正常 URL 里,Vite 在处理内部虚拟模块时会用到它。在 glob 匹配环节里,NUL 字符有特殊的语义,会导致匹配到的路径被截断或产生异常行为,让检查函数拿到的路径变成了“白名单内的某个短路径”,而实际传给文件读取操作的还是完整的越权路径。
第二条里的%2f%2f是双重编码斜杠。URL 解码后路径里会出现//,不同库对连续斜杠的处理方式不一致。检查方规范化出来的路径和作用在文件系统上的解析结果在根目录、相对路径的判定上产生了偏差,同样能达到绕过白名单的效果。
2.3 根因:没有统一“真实路径解析器”
我研究过好几个类似的路径穿越类漏洞,它们的根因都有共通之处:安全校验时用的是一套路径解析逻辑,实际执行文件操作时用的是另一套路径解析逻辑。
Chokidar 的 glob 匹配器在处理通配符、NUL、连续斜杠时,和 Node.js 的fs.realpath、path.normalize对路径的解析是两套规则。攻击者不需要搞出什么复杂魔法,只需要找到一个“两边解析结果不一致”的字符串就行,%2f、__x00__都是这类字符串。
这也是为什么修复这个漏洞不能简单地在前面加一个黑名单正则或者过滤某个关键字。过滤了__x00__,攻击者可以换%2f;过滤了%2f,可能还有别的编码组合。真正可靠的做法,是把“路径规范化”放在最前面,让后续所有环节都基于同一个规范化后的绝对路径来工作。这也是官方补丁的核心思路。
2.4 影响链路总结
梳理一下整个攻击链路:
- 攻击者向 dev server 发送一个编码过的 URL,其中包含
@id或@fs前缀。 - Vite 对 URL 进行解码和路径处理,过程中 glob 检查使用的路径被特殊字符干扰,产生误判。
- 误判让请求通过了
server.fs.allow白名单检查。 - Vite 用通过检查后的路径调用文件读取接口,实际读取的是工作目录之外的文件。
- 文件内容以 HTTP 响应形式返回给攻击者。
整个链路里,攻击者唯一要做的就是“发一个请求”,不需要其他前置条件。这也是这个漏洞被评定为高危的原因。
3. 实操复现:两个 PoC 在本地验证
3.1 准备一个隔离的测试环境
先说明一下,所有验证都在本地隔离环境进行,不要拿这个去打未授权目标。本地复现的目的只有一个:确认“白名单可以被绕过”这个结论是真的。
我用的是 Node.js 20,Linux 环境。下面这套流程在 macOS 和 Windows 上同样适用,区别只是最后读取的系统文件路径不同。
mkdir cve-2025-32395-lab cd cve-2025-32395-lab npm create vite@latest . -- --template vanilla然后装一个受影响版本。当时我锁的是 6.2.6:
npm install npm install -D vite@6.2.6 npx vite --host 127.0.0.1 --port 5173启动后,dev server 会监听在127.0.0.1:5173。
这里有个小体会:不少人习惯用npm create vue@latest或npm create vite拿最新模板,结果依赖一升级就自动跳到修复版本,反而复现不成功。要复现这个 CVE,务必把 vite 锁到受影响版本,比如vite@6.2.6。
3.2 用 curl 验证第一个 PoC
先看@id路径的 PoC:
curl --path-as-is "http://127.0.0.1:5173/@id/__x00__/etc/passwd"如果你的命令结果不是空白,说明响应里带了文件内容。在 Linux 上,正常会直接读到/etc/passwd的内容;如果 Vite 把它当模块处理,返回的可能是带 JS 包装的文本,这时候可以加上?raw参数试试:
curl --path-as-is "http://127.0.0.1:5173/@id/__x00__/etc/passwd?raw"第一次复现成功后我愣了一下,因为从结果看,好像只是发了一个请求,文件内容就原样回来了。没有权限判断,没有二次确认。这就是这个漏洞最现实的地方:利用成本极低。
3.3 第二个 PoC:双编码斜杠
再试@fs路径的 PoC:
curl --path-as-is "http://127.0.0.1:5173/@fs/%2f%2fetc%2fpasswd"这里%2f是编码后的/,整个 URL 在 curl 层面要用--path-as-is保留原始路径格式。有的环境里第二个 PoC 可能返回 403,这和 Vite 具体小版本、操作系统、路径归一化的细节都有关。两个 PoC 在同一个环境不一定同时有效,但只要有一条能读出/etc/passwd,就足以说明漏洞存在。
Windows 上复现的话,目标可以换成C:\Windows\win.ini。路径编码要一起调整,比如/@fs/%2f%2fC:%2fWindows%2fwin.ini,注意盘符和斜杠的编码方式。我自己在 Windows 上测试时,更稳定的是直接用第一个 PoC,把/etc/passwd的部分换成C:/Windows/win.ini。
curl --path-as-is "http://127.0.0.1:5173/@id/__x00__/C:/Windows/win.ini"3.4 浏览器里也可以复现
如果你不想用 curl,直接在浏览器地址栏输入这些 URL 也一样:
http://127.0.0.1:5173/@id/__x00__/etc/passwd http://127.0.0.1:5173/@fs/%2f%2fetc%2fpasswd浏览器会自动解码部分编码,不过不影响结果。页面可能会显示文件内容文本,也可能会因为响应类型问题被当作 HTML 渲染成空白页,这时候按 F12 看 Network 面板里的响应体,内容都在。
复现完成后记得关掉 dev server。这个实验本身只是为了确认“白名单被绕过”这件事是真实存在的,验证完就结束,不要顺手在别人的机器上乱试。
4. 修复方案与防护加固
4.1 官方补丁版本是首选方案
修复这个漏洞的最直接方式,就是把 Vite 升级到官方修复版本。
我的做法是先把项目里的 vite 版本锁定到补丁版本,再跑一遍完整回归:
npm install -D vite@^6.2.7 # 或者对应 5.x 系列的话 npm install -D vite@^5.4.17升级后验证一下:
curl --path-as-is "http://127.0.0.1:5173/@id/__x00__/etc/passwd" curl --path-as-is "http://127.0.0.1:5173/@fs/%2f%2fetc%2fpasswd"修复后这两个请求应该都会返回 403 或 404。升级之后,原先的 PoC 就不再生效,说明检查逻辑已经统一了路径解析。
在 monorepo 里用 pnpm 的话,注意要pnpm up vite,并且检查所有工作区的版本,光改根目录的 package.json 是不够的。子包里单独锁了旧版本的情况很常见,如果你用的是 pnpm workspaces,可以执行:
pnpm -r exec npm ls vite把输出里所有旧版本都找出来统一升级。
4.2 临时防护:配置层面还能做什么
有些项目不敢随便升版本,怕出现兼容性问题,这种情况可以先做临时防护。注意,临时防护只是过渡,最终还是要升级。
第一个动作是把 dev server 的监听地址收回到本机:
// vite.config.js export default { server: { host: '127.0.0.1', port: 5173, }, }不要用host: '0.0.0.0'。如果确实需要局域网访问,也要确保前面有网关做 ACL 控制,只对可信 IP 开放。
第二个动作是在反向代理层拦截可疑路径。如果你在用 Nginx 或其他代理统一转发 dev server 的流量,可以加一条规则,拦截包含/@fs/或/@id/的请求:
location ~ ^/(@fs|@id) { return 403; }这个方案会对所有走代理的请求生效,但 Vite 内部的一些合法模块加载也依赖@id前缀,所以上线前一定要回归测试,看看有没有影响正常的依赖预构建和模块加载。
第三个动作是补全server.fs.deny配置,把敏感文件默认挡在外面:
export default { server: { fs: { deny: ['.env', '.env.*', '*.pem', '**/.git/**'], }, }, }顺便提一句vite在 Linux 上调用xdg-open的问题。开发机如果没装桌面环境,配置了server.open: true之后终端会报错,但 dev server 本身照常启动。很多人以为“启动失败了”,其实服务已经挂在指定端口上。这种情况下尤其要注意监听地址,因为服务往往比你预想的更早暴露到了网络里。
4.3 怎么自查项目是否受影响
如果你手上项目很多,不想一个个点开看,可以用命令快速筛:
npm ls vite有多个项目的话,写个简单的循环检查 lock 文件,找出所有低于6.2.7/5.4.17的版本:
grep -o '"vite": "[^"]*"' package-lock.json | sort -u这个命令能列出锁定的版本号,和修复版本对一下就能判断。
另外建议用 GitHub 的 Dependabot 或 Renovate 这类依赖自动更新工具,让它在检测到新补丁时自动开 PR。依赖更新这个事,靠人肉记忆不靠谱,尤其是前端工具链这种升级频率高的领域。
4.4 纵深防御的几个习惯
升级只是第一步。这个漏洞暴露出来的本质问题是:dev server 是一个有文件读取能力的 HTTP 服务,它不应该被当作完全可信的本地服务来对待。
我现在的做法是:
- dev server 默认只监听
127.0.0.1,谁也不破例。 - 需要远程访问时,走带鉴权的代理,而不是直接暴露端口。
- 不在 dev server 进程运行时泄露环境变量文件,敏感变量统一放到
.env.local,并且确认server.fs.deny把.env*挡住。 - 在 CI 里加一条依赖安全扫描任务,遇到工具链漏洞能第一时间收到通知。
这些习惯单独看都是小事,但合在一起,遇到类似 CVE 的时候能省掉很多麻烦。
5. 常见问题与排查经验
5.1 复现过程中的问题速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 两个 PoC 都返回 403 | Vite 版本已经是修复版本 | npm ls vite检查版本,确认是 6.2.7 / 5.4.17 以上 |
| curl 返回 HTML 而不是文件内容 | 没有加?raw,Vite 按模块处理了请求 | 在 URL 末尾加?raw或?import&raw再试 |
| 请求返回 502 / 连接失败 | dev server 没启动,或端口不一致 | 确认监听端口,curl -v看连接过程 |
| Windows 下 PoC 不生效 | 路径格式没有按 Windows 盘符调整 | 用/etc/passwd的格式直接套 Windows 路径大多不行,需要写盘符和反斜杠的编码 |
| 通过代理访问复现不上 | 反向代理把%2f解码了一次,Vite 收到的已经是被“修正”过的 URL | 绕过代理直连 dev server 端口测试 |
项目本身报了和xlsx-style等历史包相关的加载错误 | 某些老 npm 包的模块格式与 Vite 默认解析规则冲突 | 这是独立的依赖兼容性问题,用optimizeDeps.include或 alias 解决,不要为此降低 vite / chokidar 版本 |
最后一条多说两句。xlsx-style这类停止维护很久的 npm 包,在 Vite 里加载时经常遇到Buffer is not defined或者 CommonJS 导出异常。网上搜 “vite 使用 xlsx-style” 能搜到各种折腾记录。很多人为了让它跑起来,会在依赖里降级 vite 或者手动改 node_modules,遇到 CVE-2025-32395 的时候就会陷入两难:升级 Vite 怕老包又崩,不升级又怕被扫出漏洞。我的建议是:老包兼容问题用 alias 和define配置去解决,不要让一个历史包袱决定你的工具链版本。Vite 的补丁版本基本不会改模块加载的核心行为,升级的风险远比你想象的小。
5.2 如何排查是否已被尝试利用
如果你管理的环境已经跑了一段时间,想确认之前有没有被扫过,可以从两个方向查。
第一步看访问日志。Vite dev server 默认不打印详细请求日志,但如果你有反向代理或中间件,翻代理日志里有没有大量带%2f、__x00__、@fs、@id字样的请求。这类特征本身不一定是攻击,因为 Vite 内部也会用@id,但如果是来自非本机 IP 的密集请求,就需要留意。
第二步是检查工作目录外的敏感文件是否有被读取的痕迹。这个在本地不太好查,但如果你把 dev server 挂在了共享目录或容器里,审计容器宿主机上的/etc/passwd、.ssh目录的访问时间,或者直接看文件系统的 access log,会有线索。
还有一个最实际的排查动作:把 dev server 停掉,用修复版本重新启动,然后观察应用是否有异常。如果业务代码没有变化,只是 Vite 升级了,相关功能都正常,那基本可以判断现状是安全的。
5.3 dev server 暴露面:一个容易被低估的安全死角
我在开头说过,dev server 早就不是“本地服务”这么简单了,这里再展开讲一下,因为它直接决定了这个漏洞的严重程度。
很多团队用 Vite 跑内部工具,比如后管平台、低代码编辑器、可视化搭建系统。这些系统在开发阶段就把 dev server 挂在服务器上,为了图省事直接裸端口给业务方用。表面上看,访问的人都是内部同事,但一旦内网有横向渗透,dev server 就是最容易拿下的猎物。这次 CVE 能够让攻击者直接读文件,那渗透流程就变得非常简单:扫到 5173 端口,发一个请求,配置文件就到手了。
如果你在服务器上起了 Vite,最少要确认监听地址是不是0.0.0.0。用下面这个命令就能看出来:
ss -ltnp | grep 5173如果你的环境里显示的是0.0.0.0:5173,那说明对局域网内所有人开放,尽早改回127.0.0.1,或者放到网关后面加一层鉴权。
另外我提一句vite --host和server.open。测试服务器上可能没有任何桌面环境,open选项在 Linux 下走xdg-open会失败,看起来像“起不来”,其实服务已经绑定了端口。这种时候要格外注意主机和端口配置,别在无盘环境下把端口绑到外网网卡还浑然不觉。
5.4 我的一点实际操作体会
漏洞公告出来之后,我第一反应是把手上所有 Vite 项目拉一遍清单,用npm ls vite确认版本。这个过程比我想象的要繁琐,因为有些项目是队友维护的,lock 文件里的版本对不上根目录的 package.json,要手动进到子项目里看。
后来我把检查这件事固化成了一个小脚本:遍历项目目录,找出所有package-lock.json,提取node_modules/vite的版本号,再和修复版本做对比。这个脚本已经跑了几个星期,后来几个依赖相关的安全事件,也都是靠它先筛出受影响范围。
我对团队的建议很简单:Vite 升级到修复版本只是起点,更重要的是把 dev server 的暴露面收窄。收到安全公告后先看自己有没有受影响,比临时抱佛脚去处理攻击日志要省事得多。
最后再分享一个很基础但容易遗漏的点:升级完 Vite 之后,记得重启 dev server 再验证一次。我见过不少人改了 package.json,但 dev server 进程一直没重启,跑了一整天还是老代码,然后误以为修复失败了。进程不重启,升级就只是“纸面上的升级”。