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

张开发
2026/4/16 18:39:58 15 分钟阅读

分享文章

Vite 任意文件读取漏洞 | CVE-2026-39363复现研究
0x0 背景介绍Vite是一个现代前端构建工具提供极速的服务端启动和模块热更新能力。在受影响版本中Vite开发服务器的WebSocket 接口存在安全缺陷允许未经验证Origin头的连接。攻击者可以通过发送特定的vite:invoke自定义WebSocket事件来调用fetchModule函数并利用file://协议结合?raw或?inline查询参数构造请求。由于该执行路径未应用server.fs.allow等访问控制策略远程攻击者可借此读取开发服务器所在主机上的任意敏感文件内容。0x1 环境搭建Ubuntu24老朋友·inshtall.sh#!/bin/bashecho[*] 阶段1/4检查并安装基础依赖...# 检查 Docker 和 curl 是否存在不存在则尝试安装仅限 Debian/Ubuntu 系if!command-vdocker/dev/null||!command-vcurl/dev/null;thenecho[] 检测到缺少依赖正在尝试安装 docker.io 和 curl...aptupdateaptinstall-ydocker.iocurlfiecho[*] 阶段2/4创建 Vite 漏洞复现工作目录...mkdir-pvite-cve-2026-39363cdvite-cve-2026-39363||{echo[x] 创建目录失败;exit1;}echo[] 工作目录:$(pwd)echo[*] 阶段3/4生成项目文件 (package.json, index.html, Dockerfile)...# 1. 生成 package.jsoncatpackage.jsonEOF { name: vite-cve-2026-39363, version: 1.0.0, description: PoC environment for CVE-2026-39363 (Vite 8.0.4), scripts: { dev: vite --host }, devDependencies: { vite: 8.0.4 } } EOF# 2. 生成 index.htmlcatindex.htmlEOF !DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleVite CVE-2026-39363 PoC/title /head body h1Vite CVE-2026-39363 (Arbitrary File Read) PoC/h1 pThis environment runs Vite 8.0.4, which is vulnerable to CVE-2026-39363./p pServer is running on span idport5173/span/p /body /html EOF# 3. 生成 DockerfilecatDockerfileEOF FROM node:20-bullseye WORKDIR /app # 复制锁文件和清单文件 COPY package.json index.html ./ # 安装依赖 RUN npm install # 暴露 Vite 默认端口 EXPOSE 5173 # 启动 Vite 开发服务器监听所有接口 CMD [npm, run, dev] EOFecho[*] 阶段4/4构建 Docker 镜像并启动容器...# 构建镜像并后台运行将容器的 5173 映射到宿主机的 5173dockerbuild-tvite-poc:8.0.4.\dockerrun-d-p5173:5173--namevite-cve-2026-39363-container vite-poc:8.0.4echoechoecho Vite CVE-2026-39363 漏洞环境部署完成echo - 访问地址: http://localhost:5173echo - 容器名称: vite-cve-2026-39363-containerecho - 漏洞版本: Vite 8.0.4 (已知存在任意文件读取漏洞)echoecho - 验证步骤:echo 1. 打开浏览器访问 http://localhost:5173echo 2. 参考https://github.com/Kai-One001/cve-/blob/main/Vite_Read_file_cve_2026_39363.mdecho0x2 漏洞复现2.1-手动复现手动复现过程参考https://github.com/Kai-One001/cve-/blob/main/Vite_Read_file_cve_2026_39363.md2.2 场景HTTP 路径验证 server.fs.* 能阻断基线前提Vite dev server处于 server.host暴露状态且server.ws未关闭步骤建议用于对比验证1.配置server.fs.stricttrue并将 server.fs.allow 限制为一个不包含敏感文件的集合。2.启动 dev server使其能被远程访问。3.触发 HTTP transform 路径读取测试文件使用 fs 路由是最直观的验证方式。 关键接口 GET /fs/TARGET_ABSOLUTE_PATH?raw 流量特征 •返回状态403Restricted •返回体为受限提示页由 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){constparsedUrlnewURL(http://example.com${req.url!})returnhasValidToken(config,parsedUrl)}// We allow non-browser requests to connect without a tokenreturntrue这会导致什么问题呢双重标准的安全检查如果是浏览器发起的请求一定会有Origin头就会检查token如果是自定义客户端比如写的脚本可以故意不发Origin头直接绕过token检查逻辑上的自相矛盾设计文档说用query参数传token可能被日志记录但重建token足够安全实际代码却说没有Origin的请求程序允许不检查token结果就是安全边界彻底混乱了设计者想的token验证确保只有授权用户能连接实际实现的token Origin同时存在才验证攻击者发现的不发Origin就能完全绕过验证这在威胁模型上直接动摇了未授权用户不能调用内部接口的边界3.2-[逻辑缺陷] vite:invoke 是任意调用器接着追踪第二步连接建起来后vite:invoke 到底如何被路由并调用到危险函数在packages/vite/src/node/server/ws.ts中消息处理流程解析成 JSON•parsed.typecustom•parsed.event 存在在HMR热更新模块中vite:invoke事件被注册了专门的处理器这个处理器的逻辑很简单粗暴收到vite:invoke消息就直接调用对应的函数然后把执行结果再通过WebSocket发回去channel.on?.(vite:invoke,listenerForInvokeHandler)listenerForInvokeHandler 的核心逻辑是1.从payload.id计算responseInvoke2.直接调用handleInvoke({type:custom,event:vite:invoke,data:payload})3.把结果回传给客户端还是vite:invoke事件// packages/vite/src/node/server/hmr.tslistenerForInvokeHandlerasync(payload,client){constresponseInvokepayload.id.replace(send,response)client.send({type:custom,event:vite:invoke,data:{name:payload.name,id:responseInvoke,data:(awaithandleInvoke({type:custom,event:vite:invoke,data:payload}))!}})}这段 RPC 处理没有任何访问控制语义payload.name 只要存在就能在 invokeHandlers中被索引执行handleInvoke 中constinvokeHandlerinvokeHandlers[name]awaitinvokeHandler(...args)在安全上这等价于把 WebSocket 当成了已认证的内部RPC总线但认证前提在 ws.ts 已经可被绕开前一节 再往下一层把 fetchModule 映射进 invoke handler //packages/vite/src/node/server/environment.tsthis.hot.setInvokeHandler({fetchModule:(id,importer,options){returnthis.fetchModule(id,importer,options)},...})只要能触发 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.tsfunctionisServerAccessDeniedForTransform(config,id){if(rawRE.test(id)||urlRE.test(id)||inlineRE.test(id)||svgRE.test(id)){returncheckLoadingAccess(config,id)!allowed}returnfalse}随后在 environment.transformRequest(url,{allowId(id){...}})中把该判断传入allowId(id){returnid[0]\0||!isServerAccessDeniedForTransform(server.config,id)}这就会导致HTTP路径的server.fs.allow/deny能影响transformRequest内部的加载/读取行为问题就在于只有HTTP路径走了这套检查流程而WebSocket-RPC路径完全绕过了它。3.3.2 fetchModule的执行路径没有传入allowId回到漏洞关键点packages/vite/src/node/ssr/fetchModule.ts它在获取url后调用letresultawaitenvironment.transformRequest(url)注意这里没有传入任何options.allowId所以packages/vite/src/node/server/transformRequest.ts中的Denied ID前置检查不会触发if(options.allowId!options.allowId(id)){consterr:anynewError(Denied ID${id})err.codeERR_DENIED_ID...throwerr}那么也就是HTTP路径做了按allowId限制读取的限制WebSocket-RPC路径直接绕限制进入transform pipeline方法。3.3.3-为什么绕开 isFileLoadingAllowed 仍能读到文件即使transformRequest中还有一个fs fallback当pluginContainer.load(id)返回null时它会按照这个逻辑codeawaitfsp.readFile(file,utf-8)// 前提是 isFileLoadingAllowed(...) 或 consumer server但漏洞利用的关键在于?raw/?inline不会让pluginContainer.load(id)返回null。相反它会被packages/vite/src/node/plugins/asset.ts的assetPlugin直接命中并返回可执行代码在assetPlugin的 load.handler中存在明显的危险分支// packages/vite/src/node/plugins/asset.tsif(rawRE.test(id)){constfilecheckPublicFile(id,config)||cleanUrl(id)return{code:export default${JSON.stringify(awaitfsp.readFile(file,utf-8),)},moduleType:js}}同时fileToDevUrl()内联分支中inlineRE.test(id)会无条件读取文件if(inlineRE.test(id)){constcontentawaitfsp.readFile(file)returnassetToDataURL(environment,file,content)}最后一道失守的防线这里的致命点不是transformRequest缺了isFileLoadingAllowed而是更上游的插件加载语义就把访问控制完全绕开assetPlugin的?raw分支直接fsp.readFile(…)它没有调用isFileLoadingAllowed()/checkLoadingAccess()transformRequest的fs fallback那段本该兜底校验的代码根本不会执行assetPlugin直接把?raw/?inline变成了无校验磁盘读3.4-[设计者预期 vs 实际实现] 内部 RPC却没有把内部化当成安全边界把这条链路放回威胁模型它暴露了两个层面的断裂1.入口断裂 ws.ts 把 token 校验绑定在 Origin header 存在 上使得非浏览器客户端能绕过鉴权前提。2.通道断裂 HTTP 中间件的 allowId -checkLoadingAccess -isFileLoadingAllowed(server.fs.allow)只对 HTTP transform 生效WebSocket 触发的 fetchModule 直接调用 environment.transformRequest(url)不给 allowId。3.插件断裂 assetPlugin 对 ?raw/?inline 的磁盘读取没有复用通用的文件访问控制工具导致即便 transformRequest 具备 fallback仍被插件已直接加载绕过。这三者合起来最终把开发服务器只服务于受信客户端的假设打穿。3.5-[影响推导] 任意文件内容以JS模块形式回传机密泄露与潜在二次利用空间如果可以成功触发assetPlugin ?raw/?inline分支服务会把读取内容打包export default raw或 data URI/内联资源inline由于回传是在 WebSocket RPC的vite:invoke响应中客户端可以直接在响应结构里看到内容codeJS 模块代码字符串file/id/url 等元信息这样带来的最大危害是攻击者可以读取 Vite dev server 进程可访问的任意敏感文件例如环境变量文件、CI 配置、密钥/证书、应用配置等进而用于信息收集、凭据窃取与横向移动即便当前实现不直接导向 RCE它也显著扩大了二次攻击面3.6-调用链路总结注入点 - 爆发点注入点WebSocket 自定义事件vite:invoke的参数namefetchModule模块id包含file:///TARGET?raw或 ?inline -ws.tsJSON.parse -emitCustomEvent(vite:invoke, payload, socket)-hmr.tsnormalizeHotChannel.setInvokeHandler(vite:invoke)-handleInvoke()-environment.tsinvokeHandlers.fetchModule -DevEnvironment.fetchModule()-ssr/fetchModule.ts environment.moduleGraph.ensureEntryFromUrl(url)environment.transformRequest(url)// 未传 allowId -server/transformRequest.ts pluginContainer.load(id)-返回 code不会走 fs fallback -node/plugins/asset.ts ?raw 分支fsp.readFile(file,utf-8)// 无 server.fs.allow 校验复用 -回传爆发 WebSocket 响应 eventvite:invoke中的 result.code 含文件内容字符串0x4 修复建议1、升级最新版本将插件升级安全版本• 升级最新版本将组件升级至官方已修复版本及以上vite8.0.5 / vite7.3.2 / vite6.4.2 • 项目地址https://github.com/vitejs/vite2、临时防护措施减少暴露面若不需要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或至少提供强制模式以消除非浏览器绕过免责声明本文仅用于安全研究目的未经授权不得用于非法渗透测试活动。

更多文章