Vite文件读取漏洞(CVE-2025-30208,CVE-2025-31486,CVE-2025-32395)调试分析

vite的文件读取漏洞前些天在p牛的知识星球讨论很热烈,当时我就想分析一下,但是好像忘了。然后这几天做了TGCTF的题,没想到已经出来一系列CVE了,所以现在赶紧开始来分析一下。

Vite 是一个现代化的前端构建工具,由 Vue.js 作者尤雨溪开发,旨在提供更快的开发体验和更高效的构建过程。它利用浏览器原生的 ES 模块(ESM)支持,实现了快速的开发服务器启动和即时的模块热更新(HMR)。在开发阶段,Vite 通过按需加载模块和使用 esbuild 预构建依赖,显著提升了启动速度和更新效率;在生产环境中,Vite 使用 Rollup 进行代码打包,生成高度优化的静态资源。此外,Vite 提供开箱即用的配置和丰富的插件生态,支持多种前端框架(如 Vue、React、Svelte 等),是现代前端开发的优选工具。

Vite更适合现代的前端构建

https://mp.weixin.qq.com/s?__biz=MzkyMTcwNjg4Mw==&mid=2247483811&idx=1&sn=2b4403023fd911f611bf5590ea3796d6&scene=21#wechat_redirect

https://mp.weixin.qq.com/s/HMhzXqSplWa-IwpftxwTiA

CVE-2025-30208(6.2.2)

复现

环境:

1
2
3
4
5
services:
web:
image: vulhub/vite:6.2.2
ports:
- "5173:5173"
1
http://localhost:5173/@fs/etc/passwd?raw??

有后缀名时,会报错403 Restricted

1
http://localhost:5173/@fs/flag.txt?raw??

使用?import&raw??即可

1
http://localhost:5173/@fs/flag.txt?import&raw??

分析

找到漏洞点

先clone下源码,直接比较修复后的差异

可以看出修复是把传入以下验证过程的&和?替换成了空

这个判断的大概意思是先检测url是否匹配正则,如果不匹配直接放走不经过ensureServingAccess的检测,而主要绕过也是在这里,然后更多的细节就需要调试了

调试

经过无数次踩坑,最后:

1
2
3
4
pnpm install#遇到报错挨个解决,有的报错可以忽略,因为playground里有很多项目容易冲突
cd packages/vite
pnpm run build
pnpm run dev#不然调试不了

playground里随便选一个点debug,Debug debug

发现在我们断点处停下了

直接看调用栈,可以看出是一连串的middleware的调用

1
2
3
4
5
6
viteTransformMiddleware(), transform.ts:173
viteServePublicMiddleware(), static.ts:95
viteHMRPingMiddleware(), index.ts:904
viteCachedTransformMiddleware(), transform.ts:77
viteHostCheckMiddleware(), hostCheck.ts:178
Async call from HTTPINCOMINGMESSAGE

直接从viteHostCheckMiddleware开始下断点调试,

viteHostCheckMiddleware检查host是否合法

viteCachedTransformMiddleware检查缓存是否已更新,否则返回304

如果是心跳请求则返回204

主要是处理静态资源请求,满足某些条件的文件会被当成publicFiles直接serve,不经过构建/处理。

  • !publicFiles.has(...)
  • 当前请求的路径不在 publicFiles 中(即不是 public/ 中的文件)→ 跳过。
  • isImportRequest(...)
  • 类似 /src/main.ts 的模块请求 → 跳过。
  • isInternalRequest(...)
  • 类似 /@vite/client/@fs/ 这类 Vite 内部路径 → 跳过。
  • urlRE.test(...)
  • 匹配 .js?url 这类带查询参数的模块转换请求(这些是要经过模块处理的)→ 跳过。

这看起来就比较难利用了,我们继续看/@fs/情况下的next()

这里就比较重要了,可以说上面的分析都没啥用,只是我分析了我就写上去了,hhh,,。

先对url进行处理,只接受GET,同时放走了不需要渲染的//favicon.ico

然后就是删去了两个正则匹配到的部分/\bt=\d{13}&?\b//[?&]$/,替换__x00__为字符0x00(不会产生截断~笑)

然后cleanUrl会把/[?#].*$/替换成空,方便识别文件后缀/类型(一般考虑cleanUrl之后结果的不好利用,因为就是个秃的文件名)

然后检查是否是.map结尾的soursemap文件,是否以/public/开头但没被静态server捕获

此时的url为/@fs/Users/leon/flag?raw?

然后因为两个正则都没匹配上,这个正则本意是匹配**?raw**这种恶意参的,但是我们加的**?**只被删了一次,剩下的**?**干扰了正则匹配,导致绕过ensureServingAccess对是否是合法请求源码路径的检查,因为没有?raw的许多正常请求确实不符合ensureServingAccess,所以用&&来短路

1
2
export const urlRE = /(\?|&)url(?:&|$)/
export const rawRE = /(\?|&)raw(?:&|$)/

然后这个if,只要满足其中一项就能进去

1
2
3
4
5
req.headers['sec-fetch-dest'] === 'script' #判断特定header
isJSRequest(url) #检查url是否以特定后缀名结尾或者没有后缀名
isImportRequest(url) #检查是否匹配/(\?|&)import=?(?:&|$)/
isCSSRequest(url) #检查是否匹配/\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/
isHTMLProxy(url) #检查是否匹配/\?html-proxy\b/

然后再判断是不是css,进入处理css的流程,否则直接读取通过transformRequest读取url

transformRequest主要是把request和cacheKey设置进了_pendingRequests,然后他会自动检测size>0进行请求的发送,但是我断点下成这样也没有拦住她

但是我们其实不需要拦住她,因为我们知道,她就是又执行了doTransform,只不过这次environment中有了新东西

所以在doTransform里面下断点

先获取module,跟进一下

这里会把请求过对应关系的装入_unresolvedUrlToModuleMap里,所以每次要重启server。然后_resolveUrl处理一下rawUrl

主要是对后缀的处理

所以前面都是在根据加载过的文件形成的map找modle,没用,最后我们到了loadAndTransform里面

前面有一下对map的处理,进这里的load

遍历plugins,挨个尝试用相应handler去call取得的id(文件路径及参数),最后通过vite:asset获取成功

其实这里面逻辑也比较简单

总结

所以,有后缀的情况下还可以

1
2
3
sec-fetch-dest: script
/@fs/Users/leon/flag.txt?html-proxy?raw??
/@fs/Users/leon/flag.txt?import?raw??

CVE-2025-31486(6.2.3/4)

受影响版本范围 修复版本
>=6.2.0, <=6.2.4 >=6.2.5
>=6.1.0, <=6.1.3 >=6.1.4, <6.2.0
>=6.0.0, <=6.0.13 >=6.0.14, <6.1.0
>=5.0.0, <=5.4.16 >=5.4.17, <6.0.0
<=4.5.11 >=4.5.12, <5.0.0

复现

我们把版本换成6.3.3

http://localhost:5173/@fs/Users/leon/flag?.svg?.wasm?init

/@fs/Users/leon/flag.txt?import&.svg?.wasm?init

都成功了

分析

这里的?.svg?.wasm?init末尾没有引号,不会被urlWithoutTrailingQuerySeparators影响,而transformRequest也能正常解析,被wasm-helper解析成功了

检测了是否以.wasm?init结尾

然后fileToUrl把文件内容转换成了data协议的url形式,造成了文件内容泄露

总结

感觉这种用plugin处理url的方法挺容易出问题啊hh

6.3.4增加了对?inline恶意调用plugin的禁用

CVE-2025-32395(6.2.5)

受影响版本范围 修复版本
>=6.2.0, <=6.2.5 >=6.2.6
>=6.1.0, <=6.1.4 >=6.1.5, <6.2.0
>=6.0.0, <=6.0.14 >=6.0.15, <6.1.0
>=5.0.0, <=5.4.17 >=5.4.18, <6.0.0
<=4.5.12 >=4.5.13, <5.0.0

版本换成6.2.5

这个版本直接把transform.ts检查恶意访问的几个正则匹配封装了,同时增加了对?.svg?.wasm?init绕过的检查

复现

/@fs/Users/leon/Desktop/vite/#/../../../flag?import

这个回显就很舒服了

但是不能用浏览器发,否则不能正常发送#

分析

HTTP 1.1规范(RFC 9112)规定在请求目标(request-target)中不允许出现#字符。然而,攻击者是有可能发送包含#的请求的。对于那些请求行(包含请求目标)无效的请求,规范建议服务器返回400错误请求)或301永久重定向)状态码来拒绝这些请求。对于HTTP 2协议也是类似的情况。

如果是在Node或Bun运行Vite环境下,当收到包含#的请求时,这些运行时内部不会拒绝该请求,而是将其传递到用户代码层面。在这种情况下,http.IncomingMessage.url的值会包含#字符。而Vite在检查server.fs.deny时,假定req.url不会包含#字符,这就导致了包含#的这类请求能够绕过server.fs.deny的检查。

先打/@fs/Users/leon/Desktop/vite/../../flag

因为没有使用任何插件,所以其实这个检测对这次payload无效

路径最终会被处理,id为/Users/le0n/flag

因为没有使用插件,最终进入了下一个回调,专门处理/@fs/

这里检查ensureServingAccess路径是否合法,但是如果此时我们传入#,url.pathname为/@fs/Users/leon/Desktop/vite/,直接绕过了检查

后面仍然会提供flag的访问

总结

这次的绕过和前面几次不同,是出在处理#而不是恶意调用plugin上的

6.2.6增加的对使用#的拦截