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

0x0 背景介绍

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

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

0x1 环境搭建(Ubuntu24)

bash 复制代码
#!/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 curl
fi

echo "[*] 阶段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.json
cat > 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.html
cat > 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. 生成 Dockerfile
cat > Dockerfile <<'EOF'
FROM node:20-bullseye

WORKDIR /app

# 复制锁文件和清单文件
COPY package.json index.html ./
# 安装依赖
RUN npm install

# 暴露 Vite 默认端口
EXPOSE 5173

# 启动 Vite 开发服务器,监听所有接口
CMD ["npm", "run", "dev"]
EOF

echo "[*] 阶段4/4:构建 Docker 镜像并启动容器..."
# 构建镜像并后台运行,将容器的 5173 映射到宿主机的 5173
docker 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 ""
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-手动复现

  • 手动复现,过程参考
bash 复制代码
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未关闭

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

bash 复制代码
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,即允许连接:

ts 复制代码
// packages/vite/src/node/server/ws.ts
if(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 token
return 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:
bash 复制代码
•parsed.type === 'custom'
•parsed.event 存在

在HMR热更新模块中,vite:invoke事件被注册了专门的处理器

这个处理器的逻辑很简单粗暴:收到vite:invoke消息,就直接调用对应的函数

然后把执行结果再通过WebSocket发回去

ts 复制代码
channel.on?.('vite:invoke', listenerForInvokeHandler)

listenerForInvokeHandler 的核心逻辑是:

javascript 复制代码
1.从payload.id计算responseInvoke

2.直接调用handleInvoke({type:'custom', event:'vite:invoke', data: payload})

3.把结果回传给客户端(还是vite:invoke事件)
ts 复制代码
// packages/vite/src/node/server/hmr.ts
listenerForInvokeHandler = 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中被索引执行
ts 复制代码
handleInvoke 中 const invokeHandler = invokeHandlers[name] + await invokeHandler(...args)
在安全上,这等价于把 
WebSocket 
当成了"已认证的内部 
RPC
 总线",但认证前提在 
ws.ts
 已经可被绕开(前一节)
再往下一层,把
fetchModule
映射进 
invoke handler
:
//packages/vite/src/node/server/environment.ts
this.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,实现:

ts 复制代码
//packages/vite/src/node/server/middlewares/transform.ts
function 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后调用:

ts 复制代码
let result = await environment.transformRequest(url)

注意:这里没有传入任何options.allowId

  • 所以packages/vite/src/node/server/transformRequest.ts中的"Denied ID"前置检查不会触发:
ts 复制代码
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时,它会按照这个逻辑:

ts 复制代码
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中,存在明显的危险分支:
ts 复制代码
// packages/vite/src/node/plugins/asset.ts
if (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)会无条件读取文件:

ts 复制代码
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"却没有把内部化当成安全边界

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

bash 复制代码
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响应中,客户端可以直接在响应结构里看到内容:

  • code(JS 模块代码字符串)

  • file/id/url 等元信息

这样带来的最大危害是:

  • 攻击者可以读取 Vite dev server 进程可访问的任意敏感文件(例如环境变量文件、CI 配置、密钥/证书、应用配置等)

  • 进而用于信息收集、凭据窃取与横向移动(即便当前实现不直接导向 RCE,它也显著扩大了二次攻击面)

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

bash 复制代码
注入点: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、升级最新版本:将插件升级安全版本
bash 复制代码
• 升级最新版本:将组件升级至官方已修复版本及以上(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",或至少提供强制模式以消除非浏览器绕过

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

相关推荐
好运的阿财2 小时前
OpenClaw四种角色详解
人工智能·python·程序人生·microsoft·开源·ai编程
AI_零食2 小时前
Flutter 框架跨平台鸿蒙开发 - 颜色听觉化应用
学习·flutter·信息可视化·开源·harmonyos
2301_822703202 小时前
大学生体质健康测试全景测绘台:基于鸿蒙Flutter的多维数据可视化与状态管理响应架构
算法·flutter·信息可视化·架构·开源·harmonyos·鸿蒙
独特的螺狮粉2 小时前
生命科学实验室经费极简记账簿:基于鸿蒙Flutter的极简主义状态响应与流式布局架构
flutter·华为·架构·开源·harmonyos
进击monkey2 小时前
装修行业 × PandaWiki:构建多端产品统一知识库,提升用户体验与运营效率
人工智能·开源·ai知识库
提子拌饭1332 小时前
红细胞代偿性增殖与睡眠剥夺的对照演算引擎:基于鸿蒙Flutter的微观流体力学粒子渲染架构
flutter·华为·架构·开源·harmonyos·鸿蒙
AI成长日志2 小时前
【GitHub开源项目】推理优化技术栈全览:从PyTorch到专用引擎
pytorch·开源·github
人间打气筒(Ada)2 小时前
「码动四季·开源同行」go语言:微服务网关如何作为服务端统一入口点?
微服务·golang·开源·微服务网关·go实战
提子拌饭1332 小时前
液相色谱质谱联用(LC-MS)数据可视化引擎:基于鸿蒙Flutter的高精度色谱卡与多维峰值拟合架构
flutter·华为·信息可视化·开源·harmonyos·鸿蒙