文章总结: 本文详细分析了Vite开发服务器中的任意文件读取漏洞CVE-2026-39363。漏洞源于WebSocket接口的Origin头验证缺陷,允许未授权连接通过vite:invoke事件调用fetchModule函数,结合file://协议绕过server.fs.allow访问控制,实现任意文件读取。文章提供了完整的Docker环境搭建步骤、手动复现方法及流量特征分析,并深入剖析了漏洞链的三大关键环节:验证绕过、RPC调用机制和访问控制失效。 综合评分: 88 文章分类: 漏洞分析,WEB安全,安全工具
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 "[*] 阶段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 curlfiecho "[*] 阶段2/4:创建 Vite 漏洞复现工作目录..."mkdir -p vite-cve-2026-39363 && cd vite-cve-2026-39363 || { echo "[x] 创建目录失败"; exit 1; }echo "[+] 工作目录: $(pwd)"echo "[*] 阶段3/4:生成项目文件 (package.json, index.html, Dockerfile)..."# 1. 生成 package.jsoncat > package.json <<'EOF'{ "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.htmlcat > index.html <<'EOF'<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vite CVE-2026-39363 PoC</title></head><body> <h1>Vite CVE-2026-39363 (Arbitrary File Read) PoC</h1> <p>This environment runs Vite 8.0.4, which is vulnerable to CVE-2026-39363.</p> <p>Server is running on <span id="port">5173</span></p></body></html>EOF# 3. 生成 Dockerfilecat > Dockerfile <<'EOF'FROM node:20-bullseyeWORKDIR /app# 复制锁文件和清单文件COPY package.json index.html ./# 安装依赖RUN npm install# 暴露 Vite 默认端口EXPOSE 5173# 启动 Vite 开发服务器,监听所有接口CMD ["npm", "run", "dev"]EOFecho "[*] 阶段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 ""echo "=============================================="echo " Vite CVE-2026-39363 漏洞环境部署完成!"echo " - 访问地址: http://localhost:5173"echo " - 容器名称: vite-cve-2026-39363-container"echo " - 漏洞版本: Vite 8.0.4 (已知存在任意文件读取漏洞)"echo ""echo " - 验证步骤:"echo " 1. 打开浏览器访问 http://localhost:5173"echo " 2. 参考:https://github.com/Kai-One001/cve-/blob/main/Vite_Read_file_cve_2026_39363.md"echo "=============================================="
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.触发 HTTP transform 路径读取测试文件(使用 @fs 路由是最直观的验证方式)。关键接口: GET /@fs/<TARGET_ABSOLUTE_PATH>?raw流量特征:•返回状态 403 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){ const parsedUrl = new URL(`http://example.com${req.url!}`) return hasValidToken(config, parsedUrl)}
// We allow non-browser requests to connect without a tokenreturn true
这会导致什么问题呢?
- 双重标准的安全检查:
- 如果是浏览器发起的请求:一定会有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.type === 'custom'•parsed.event 存在
- 在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 = async (payload, client) => { const responseInvoke = payload.id.replace('send', 'response') client.send({ type: 'custom', event: 'vite:invoke', data: { name: payload.name, id: responseInvoke, data: (await handleInvoke({ type:'custom', event:'vite:invoke', data: payload }))! } })}
这段 RPC 处理没有任何”访问控制语义”:
payload.name只要存在就能在invokeHandlers中被索引执行
handleInvoke 中 const invokeHandler = invokeHandlers[name] + await invokeHandler(...args)
- 在安全上,这等价于把 WebSocket 当成了”已认证的内部 RPC 总线”,但认证前提在
ws.ts已经可被绕开(前一节)
再往下一层,把fetchModule映射进 invoke handler:
//packages/vite/src/node/server/environment.tsthis.hot.setInvokeHandler({ fetchModule: (id, importer, options) => { return this.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.tsfunction isServerAccessDeniedForTransform(config, id) { if (rawRE.test(id) || urlRE.test(id) || inlineRE.test(id) || svgRE.test(id)) { return checkLoadingAccess(config, id) !== 'allowed' } return false}
- 随后在
environment.transformRequest(url, { allowId(id){...} })中把该判断传入:
allowId(id) { return id[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后调用:
let result = await environment.transformRequest(url)
- 注意:这里没有传入任何
options.allowId - 所以
packages/vite/src/node/server/transformRequest.ts中的”Denied ID”前置检查不会触发:
if (options.allowId && !options.allowId(id)) { const err: any = new Error(`Denied ID ${id}`) err.code = ERR_DENIED_ID ... throw err}
- 那么也就是:HTTP路径做了”按allowId限制读取”的限制;
- WebSocket-RPC路径直接绕限制进入transform pipeline方法。
3.3.3-为什么绕开 isFileLoadingAllowed 仍能读到文件:
即使transformRequest中还有一个fs fallback:当pluginContainer.load(id)返回null时,它会按照这个逻辑:
code = await fsp.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)) { const file = checkPublicFile(id, config) || cleanUrl(id) return { code: `export default ${JSON.stringify( await fsp.readFile(file, 'utf-8'), )}`, moduleType: 'js' }}
- 同时
fileToDevUrl()内联分支中,inlineRE.test(id)会无条件读取文件:
if (inlineRE.test(id)) { const content = await fsp.readFile(file) return assetToDataURL(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 <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的参数 name=fetchModule,模块id包含file:///<TARGET>?raw(或 ?inline) ->ws.ts:JSON.parse -> emitCustomEvent('vite:invoke', payload, socket) ->hmr.ts:normalizeHotChannel.setInvokeHandler('vite:invoke') -> handleInvoke() ->environment.ts:invokeHandlers.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 响应 event 'vite:invoke' 中的 result.code 含文件内容字符串
#
0x4 修复建议
1、升级最新版本:将插件升级安全版本
• 升级最新版本:将组件升级至官方已修复版本及以上(vite@>=8.0.5 / vite@>=7.3.2 / 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复现&研究》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论