Vite任意文件读取漏洞|CVE-2026-39363复现&研究

admin 2026-04-10 02:13:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档分析了Vite开发服务器存在的任意文件读取漏洞(CVE-2026-39363),影响版本包括vite6.x至8.x。漏洞成因是WebSocket接口未验证Origin头,允许攻击者通过vite:invoke事件调用fetchModule函数,并利用file://协议绕过server.fs.allow访问控制读取主机敏感文件。文章提供了环境搭建步骤、复现流量特征及详细原理分析,指出HTTP路径与WebSocket路径的访问控制差异导致安全边界失效。 综合评分: 85 文章分类: 漏洞分析,WEB安全,渗透测试,安全工具,应急响应


cover_image

Vite 任意文件读取漏洞 | CVE-2026-39363复现&研究

原创

404号浪漫 404号浪漫

404号浪漫

2026年4月8日 23:09 北京

点击蓝字,关注我们

0x0 背景介绍

Vite是一个现代前端构建工具,提供极速的服务端启动和模块热更新能力。

在受影响版本中,Vite开发服务器的WebSocket 接口存在安全缺陷,允许未经验证Origin头的连接。攻击者可以通过发送特定的vite:invoke自定义WebSocket事件来调用fetchModule函数,并利用file://协议结合?raw或?inline查询参数构造请求。由于该执行路径未应用server.fs.allow等访问控制策略,远程攻击者可借此读取开发服务器所在主机上的任意敏感文件内容。

漏洞详情

| 漏洞类型 | 影响版本 | 利用复杂度 | CVE编号 | | — | — | — | — | | 文件读取 | vite-plus ≤0.1.15 vite 6.x ≤6.4.1 vite 7.x ≤7.3.1 vite 8.x ≤8.0.4 | 低 | CVE-2026-39363 |

攻击效果:

  • 读取敏感文件数据。

0x1 环境搭建(Ubuntu24)

1.1-Ubuntu24+Docker搭建配置

  • ### 依旧是inshtall.sh
#!/bin/bashecho&nbsp;"[*] 阶段1/4:检查并安装基础依赖..."# 检查 Docker 和 curl 是否存在,不存在则尝试安装(仅限 Debian/Ubuntu 系)if&nbsp;!&nbsp;command&nbsp;-v docker &> /dev/null || !&nbsp;command&nbsp;-v curl &> /dev/null;&nbsp;then&nbsp; &nbsp;&nbsp;echo&nbsp;"[+] 检测到缺少依赖,正在尝试安装 docker.io 和 curl..."&nbsp; &nbsp; apt update && apt install -y docker.io curlfiecho&nbsp;"[*] 阶段2/4:创建 Vite 漏洞复现工作目录..."mkdir&nbsp;-p vite-cve-2026-39363 &&&nbsp;cd&nbsp;vite-cve-2026-39363 || {&nbsp;echo&nbsp;"[x] 创建目录失败";&nbsp;exit&nbsp;1; }echo&nbsp;"[+] 工作目录:&nbsp;$(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"]EOFecho&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.4echo&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;"=============================================="

0x2 漏洞复现

2.1-复现过程

  • 手动复现,过程参考
https://github.com/Kai-One001/cve-/blob/main/Vite_Read_file_cve_2026_39363.md

2.2 场景:HTTP 路径验证 server.fs.* 能阻断(基线)

前提:Vite dev server处于 server.host暴露状态,且server.ws未关闭

步骤(建议用于对比验证)

1.配置 server.fs.strict=true,并将 server.fs.allow 限制为一个不包含敏感文件的集合。2.启动 dev server,使其能被远程访问。3.触发&nbsp;HTTP&nbsp;transform 路径读取测试文件(使用&nbsp;@fs&nbsp;路由是最直观的验证方式)。关键接口:&nbsp;GET&nbsp;/@fs/<TARGET_ABSOLUTE_PATH>?raw流量特征:•返回状态&nbsp;403&nbsp;Restricted•返回体为受限提示页(由 respondWithAccessDenied 渲染)

#

2.3-复现流量特征 (PCAP)

  • ### 协议是websocket的,但是相应能看到具体请求文件名称和值

#

#

#

#


0x3 漏洞原理分析

3.1-[入口] “谁都可以连”:

先从入口问一个侦探式问题:漏洞链条第一步到底依赖什么身份验证? 在Vite的WebSocket服务器创建时,有一个shouldHandle()函数,它决定是否允许普通HTTP连接升级为WebSocket

关键在这里:hasValidToken()只在”请求头里存在Origin“时才会被调用;而当Origin不存在时,函数会直接return true,即允许连接:

// packages/vite/src/node/server/ws.tsif(req.headers.origin){&nbsp;&nbsp;const&nbsp;parsedUrl =&nbsp;new&nbsp;URL(`http://example.com${req.url!}`)&nbsp;&nbsp;return&nbsp;hasValidToken(config, parsedUrl)}
// We allow non-browser requests to connect without a tokenreturn&nbsp;true

这会导致什么问题呢?

  1. 双重标准的安全检查
  • 如果是浏览器发起的请求:一定会有Origin头,就会检查token
  • 如果是自定义客户端(比如写的脚本):可以故意不发Origin头,直接绕过token检查
  1. 逻辑上的自相矛盾
  • 设计文档说”用query参数传token可能被日志记录,但重建token足够安全”
  • 实际代码却说”没有Origin的请求,程序允许不检查token”

结果就是安全边界彻底混乱了:

  • 设计者想的:token验证确保只有授权用户能连接
  • 实际实现的:token + Origin同时存在才验证
  • 攻击者发现的:不发Origin就能完全绕过验证
  • 这在威胁模型上直接动摇了”未授权用户不能调用内部接口”的边界

3.2-[逻辑缺陷] vite:invoke 是”任意调用器”:

接着追踪第二步:连接建起来后,vite:invoke 到底如何被路由并调用到危险函数?packages/vite/src/node/server/ws.ts中,消息处理流程解析成 JSON:

•parsed.type ===&nbsp;'custom'•parsed.event&nbsp;存在
  • 在HMR热更新模块中,vite:invoke事件被注册了专门的处理器
  • 这个处理器的逻辑很简单粗暴:收到vite:invoke消息,就直接调用对应的函数
  • 然后把执行结果再通过WebSocket发回去
channel.on?.('vite:invoke', listenerForInvokeHandler)

listenerForInvokeHandler 的核心逻辑是:

  • 1.从payload.id计算responseInvoke
  • 2.直接调用handleInvoke({type:'custom', event:'vite:invoke', data: payload})
  • 3.把结果回传给客户端(还是vite:invoke事件)
// packages/vite/src/node/server/hmr.tslistenerForInvokeHandler =&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;type:'custom',&nbsp;event:'vite:invoke',&nbsp;data: payload }))!&nbsp; &nbsp; }&nbsp; })}

这段 RPC 处理没有任何”访问控制语义”:

  • payload.name 只要存在就能在 invokeHandlers 中被索引执行
handleInvoke 中&nbsp;const&nbsp;invokeHandler = invokeHandlers[name] +&nbsp;await&nbsp;invokeHandler(...args)
  • 在安全上,这等价于把 WebSocket 当成了”已认证的内部 RPC 总线”,但认证前提在 ws.ts 已经可被绕开(前一节)

再往下一层,把fetchModule映射进 invoke handler:

//packages/vite/src/node/server/environment.tsthis.hot.setInvokeHandler({&nbsp; fetchModule: (id, importer, options) => {&nbsp; &nbsp;&nbsp;return&nbsp;this.fetchModule(id, importer, options)&nbsp; },&nbsp; ...})
  • 只要能触发 vite:invoke,就能远程调用 DevEnvironment.fetchModule()
  • ### 把 WebSocket 的自定义事件转成了对 fetchModule 的远程执行

#

3.3-[攻击链路] fetchModule 不经过 HTTP 访问控制

现在锁定第三个关键缺口:访问控制到底在哪个环节生效?

· 在普通的HTTP请求路径中,访问控制是正常工作的· 但在WebSocket-RPC这条特殊路径中,访问控制机制完全失效了

3.3.1 HTTP 变换中间件的控制逻辑是”按查询语义拦截”

  • 定义了rawRE/urlRE/inlineRE,实现:
//packages/vite/src/node/server/middlewares/transform.tsfunction&nbsp;isServerAccessDeniedForTransform(config,&nbsp;id) {&nbsp;&nbsp;if&nbsp;(rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) {&nbsp; &nbsp;&nbsp;return&nbsp;checkLoadingAccess(config,&nbsp;id) !==&nbsp;'allowed'&nbsp; }&nbsp;&nbsp;return&nbsp;false}
  • 随后在environment.transformRequest(url, { allowId(id){...} })中把该判断传入:
allowId(id) {&nbsp;&nbsp;return&nbsp;id[0] ===&nbsp;'\0'&nbsp;|| !isServerAccessDeniedForTransform(server.config,&nbsp;id)}
  • 这就会导致HTTP路径的server.fs.allow/deny能影响transformRequest内部的加载/读取行为
  • 问题就在于:只有HTTP路径走了这套检查流程,而WebSocket-RPC路径完全绕过了它。

3.3.2  fetchModule的执行路径没有传入allowId

回到漏洞关键点:packages/vite/src/node/ssr/fetchModule.ts,它在获取url后调用:

let&nbsp;result =&nbsp;await&nbsp;environment.transformRequest(url)
  • 注意:这里没有传入任何options.allowId
  • 所以packages/vite/src/node/server/transformRequest.ts中的”Denied ID”前置检查不会触发:
if&nbsp;(options.allowId&nbsp;&& !options.allowId(id)) {&nbsp;&nbsp;const&nbsp;err:&nbsp;any&nbsp;=&nbsp;new&nbsp;Error(`Denied ID&nbsp;${id}`)&nbsp; err.code&nbsp;=&nbsp;ERR_DENIED_ID&nbsp; ...&nbsp;&nbsp;throw&nbsp;err}
  • 那么也就是:HTTP路径做了”按allowId限制读取”的限制;
  • WebSocket-RPC路径直接绕限制进入transform pipeline方法。

3.3.3-为什么绕开 isFileLoadingAllowed 仍能读到文件:

即使transformRequest中还有一个fs fallback:当pluginContainer.load(id)返回null时,它会按照这个逻辑:

code =&nbsp;await&nbsp;fsp.readFile(file,&nbsp;'utf-8')// 前提是 isFileLoadingAllowed(...) 或 consumer === 'server'
  • 但漏洞利用的关键在于?raw/?inline不会让pluginContainer.load(id)返回null
  • 相反,它会被packages/vite/src/node/plugins/asset.tsassetPlugin直接命中并返回可执行代码。

assetPlugin的 load.handler中,存在明显的危险分支:

// packages/vite/src/node/plugins/asset.tsif&nbsp;(rawRE.test(id)) {&nbsp;&nbsp;const&nbsp;file =&nbsp;checkPublicFile(id, config) ||&nbsp;cleanUrl(id)&nbsp;&nbsp;return&nbsp;{&nbsp; &nbsp;&nbsp;code:&nbsp;`export default&nbsp;${JSON.stringify(&nbsp; &nbsp; &nbsp;&nbsp;await&nbsp;fsp.readFile(file,&nbsp;'utf-8'),&nbsp; &nbsp; )}`,&nbsp; &nbsp;&nbsp;moduleType:&nbsp;'js'&nbsp; }}
  • 同时fileToDevUrl()内联分支中,inlineRE.test(id)会无条件读取文件:
if&nbsp;(inlineRE.test(id)) {&nbsp;&nbsp;const&nbsp;content =&nbsp;await&nbsp;fsp.readFile(file)&nbsp;&nbsp;return&nbsp;assetToDataURL(environment,&nbsp;file, content)}

最后一道失守的防线

这里的致命点不是”transformRequest缺了isFileLoadingAllowed”,而是更上游的插件加载语义就把访问控制完全绕开

  • assetPlugin?raw分支直接fsp.readFile(...)
  • 它没有调用isFileLoadingAllowed()/checkLoadingAccess()
  • transformRequest的fs fallback(那段本该兜底校验的代码)根本不会执行
  • assetPlugin直接把?raw/?inline变成了无校验磁盘读

3.4-[设计者预期 vs 实际实现] “内部 RPC”却没有把内部化当成安全边界

把这条链路放回威胁模型,它暴露了两个层面的断裂:

1.入口断裂:ws.ts 把 token 校验绑定在&nbsp;Origin&nbsp;header 存在 上,使得非浏览器客户端能绕过鉴权前提。2.通道断裂:HTTP&nbsp;中间件的 allowId -> checkLoadingAccess -> isFileLoadingAllowed(server.fs.allow)只对&nbsp;HTTP&nbsp;transform 生效;WebSocket&nbsp;触发的 fetchModule 直接调用 environment.transformRequest(url),不给 allowId。3.插件断裂:assetPlugin 对&nbsp;?raw/?inline 的磁盘读取没有复用通用的文件访问控制工具,导致即便 transformRequest 具备 fallback,仍被"插件已直接加载"绕过。
  • 这三者合起来,最终把”开发服务器只服务于受信客户端”的假设打穿。

3.5-[影响推导] 任意文件内容以JS模块形式回传:机密泄露与潜在二次利用空间

  • 如果可以成功触发assetPlugin ?raw/?inline分支,服务会把读取内容打包:
•&nbsp;export&nbsp;default&nbsp;<stringified file content>(raw)
• 或 data URI/内联资源(inline)
  • 由于回传是在 WebSocket RPC的vite:invoke响应中,客户端可以直接在响应结构里看到内容:
• code(JS 模块代码字符串)
• file/id/url 等元信息
  • 这样带来的最大危害是:
· 攻击者可以读取 Vite dev server 进程可访问的任意敏感文件(例如环境变量文件、CI 配置、密钥/证书、应用配置等)
· 进而用于信息收集、凭据窃取与横向移动(即便当前实现不直接导向 RCE,它也显著扩大了二次攻击面)

3.6-调用链路总结(注入点 -> 爆发点)

注入点:WebSocket 自定义事件vite:invoke的参数&nbsp; name=fetchModule,模块id包含file:///<TARGET>?raw(或 ?inline)&nbsp; &nbsp; ->ws.ts:JSON.parse ->&nbsp;emitCustomEvent('vite:invoke', payload, socket)&nbsp; &nbsp; ->hmr.ts:normalizeHotChannel.setInvokeHandler('vite:invoke') ->&nbsp;handleInvoke()&nbsp; &nbsp; ->environment.ts:invokeHandlers.fetchModule -> DevEnvironment.fetchModule()&nbsp; &nbsp; ->ssr/fetchModule.ts:&nbsp; environment.moduleGraph.ensureEntryFromUrl(url)&nbsp; environment.transformRequest(url) &nbsp; // 未传 allowId&nbsp; &nbsp; ->server/transformRequest.ts:&nbsp; pluginContainer.load(id) -> 返回 code(不会走 fs fallback)&nbsp; &nbsp; ->node/plugins/asset.ts:&nbsp; ?raw 分支:fsp.readFile(file,&nbsp;'utf-8') &nbsp;// 无 server.fs.allow 校验复用&nbsp; &nbsp; ->回传爆发:WebSocket 响应 event&nbsp;'vite:invoke'&nbsp;中的 result.code 含文件内容字符串

#


0x4 修复建议

1、升级最新版本:将插件升级安全版本

• 升级最新版本:将组件升级至官方已修复版本及以上(vite@>=8.0.5&nbsp;/&nbsp;vite@>=7.3.2&nbsp;/&nbsp;vite@>=6.4.2)
• 项目地址:https://github.com/vitejs/vite

2、临时防护措施:

  • 减少暴露面:若不需要HMR,配置server.ws: false禁用WebSocket

  • 防火墙 / WAF:检测并拦截WebSocket协议握手及其后续消息中包含敏感特征的流量(例如vite:invoke + fetchModule + file:// + ?raw/?inline片段)

  • 限制访问:仅允许内网或localhost访问dev server;不要用–host 0.0.0.0暴露到公网,并配合防火墙仅放行开发人员IP

  • 限最小化:在反向代理或网关层强制校验Origin,并对缺失Origin的WebSocket升级请求做拒绝;同时确保server.ws的鉴权token校验对所有连接都生效

  • 代码级修复方向:在ws.ts中把WebSocket鉴权前提从”是否存在Origin”改为”是否持有有效token”,或至少提供强制模式以消除非浏览器绕过


/**同志门!注意身体呀,北平的春好像被置换了,感觉不到**/

免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。


免责声明:

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

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

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

本文转载自:404号浪漫 404号浪漫 404号浪漫《Vite 任意文件读取漏洞 | CVE-2026-39363复现&研究》

评论:0   参与:  0