0x0 背景介绍
Vite 是一个用于 JavaScript 的前端工具框架。在 6.3.4、6.2.7、6.1.6、5.4.19 和 4.5.14 之前的版本中,项目根目录下被文件匹配模式拒绝的文件内容可能会被返回给浏览器。只有明确将Vite 开发服务器暴露给网络的应用程序(使用 --host 或 server.host 配置选项)会受到影响。只有位于项目根目录下且被文件匹配模式拒绝的文件可能被绕过。server.fs.deny 可以包含匹配文件的模式(默认情况下,它包括 .env、.env.*、*.{crt,pem} 等模式)。这些模式能够通过使用斜杠和点(/.)的组合来绕过 root 下的文件。该问题已在 6.3.4、6.2.7、6.1.6、5.4.19 和 4.5.14 版本中修复。
0x1 环境搭建
1. Ubuntu24环境复现
- 另存为install.sh并赋予执行权限chmod +x install.sh
bash
#!/bin/bash
echo "[+] 开始搭建 Vite CVE-2025-46565 漏洞复现环境"
echo "[+] 目标:使用 vite@6.3.3 复现路径遍历漏洞(/.env/.)"
# 步骤 1:创建项目目录
echo -e "\n[+] 步骤 1/7:创建项目目录"
mkdir -p vite-poc && cd vite-poc
echo "[+] 已创建目录:$(pwd)"
# 步骤 2:初始化 npm 项目
echo -e "\n[+] 步骤 2/7:初始化 npm"
npm init -y
echo "[+] package.json 已生成"
# 步骤 3:安装易受攻击的 Vite 版本
echo -e "\n[+] 步骤 3/7:安装 vite@6.3.3(受影响版本)"
npm install vite@6.3.3
echo "[+] Vite 6.3.3 安装完成"
# 步骤 4:创建 index.html
echo -e "\n[+] 步骤 4/7:创建 index.html"
cat > index.html << 'EOF_HTML'
<!DOCTYPE html>
<html>
<body>
<h1>Vite CVE-2025-46565 PoC</h1>
<p>如果能通过 /%2E.env%2E/ 读取 .env 文件,则存在漏洞。</p>
</body>
</html>
EOF_HTML
echo "✅ index.html 创建成功"
# 步骤 5:创建多个敏感文件(模拟真实项目)
echo -e "\n[+] 步骤 5/7:创建敏感配置文件"
# .env
cat > .env << 'EOF_ENV'
SECRET_KEY=ThisIsASecretPassword
DB_PASS=root123
API_TOKEN=abc123xyz
EOF_ENV
# .env.local
cat > .env.local << 'EOF_LOCAL'
DATABASE_URL=mysql://devuser:devpass@localhost/devdb
JWT_SECRET=local_jwt_secret_456
EOF_LOCAL
# ca.pem
cat > ca.pem << 'EOF_PEM'
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIUJfZ1R03qI3Q6OzJd9P/XuXjNvRMwDQYJKoZIhvcNAQELBQAw
EjEQMA4GA1UEAwwHY2EtY2VydDAeFw0yNTAxMDEwMDAwMDBaFw0zNTAxMDEwMDAwMDBa
MBIxEDAOBgNVBAMMB2NhLWNlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC9JZt1...(模拟证书内容)
-----END CERTIFICATE-----
EOF_PEM
# .npmrc
echo "[+]//registry.npmjs.org/:_authToken=abcdefg12345" > .npmrc
echo "[+] 敏感文件已创建:.env, .env.local, ca.pem, .npmrc"
# 步骤 6:创建 vite.config.js
echo -e "\n[+] 步骤 6/7:创建 vite.config.js(启用 --host)"
cat > vite.config.js << 'EOF_CONFIG'
export default {
server: {
host: '0.0.0.0',
port: 5173
}
}
EOF_CONFIG
echo "[+] vite.config.js 配置完成(允许外部访问)"
# 步骤 7:添加 dev 脚本
echo -e "\n[+] 步骤 7/7:添加启动脚本"
npm pkg set scripts.dev="vite"
echo "[+] 启动脚本已添加:npm run dev"
# 结束提示
echo -e "\n🎉 环境搭建完成!"
echo "📌 接下来你可以:"
echo ""
echo " 1️⃣ cd进入项目目录,启动 Vite 服务器:"
echo " npx vite --host"
echo ""
echo "💡 注意:保持服务器运行,不要关闭当前终端。"
2. 启动成功

0x2 漏洞复现
手动复现步骤
-
正常请求文件时,被拒绝的响应

-
绕过访问

-
多姿势绕过访问

-
示例其它文件

YML检测
bash
https://github.com/Kai-One001/cve-/blob/main/Vite-CVE-2025-46565.yml

复现流量特征 (PACP)
0、读取文件

2、多姿势绕过

0x3 漏洞原理分析
确定入口:静态文件服务
-
Vite 是一个开发服务器,处理静态文件的逻辑在:
/packages/vite/src/node/server/middlewares/static.ts -
进去后查询发现
ensureServingAccess,它是静态资源访问控制的核心入口函数,负责判断请求路径是否允许被服务 -
并且它调用了
isFileServingAllowed(url, server)来进行权限校验
ts
export function ensureServingAccess(...) {
if (isFileServingAllowed(url, server)) {
return true
}
if (isFileReadable(cleanUrl(url))) {
// → 返回 403 错误页面
} else {
next() // → 继续中间件链
}
return false
}
- 如果
isFileServingAllowed返回false,但文件不可读(不存在),就next() → 404 - 如果文件可读,就返回
403错误页面
调用 ensureServingAccess 的上下文
- 在静态中间件中,路径解析完成后立即调用
ensureServingAccess做安全检查:
ts
#静态文件服务中间件
const resolvedPathname = redirectedPathname || pathname
let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname))
if (resolvedPathname.endsWith('/') && fileUrl[fileUrl.length - 1] !== '/') {
fileUrl = withTrailingSlash(fileUrl)
}
// 安全检查:是否允许访问此路径?
if (!ensureServingAccess(fileUrl, server, res, next)) {
return
}
- 使用
path.resolve()将 URL 路径转换为本地文件系统绝对路径 - 调用
ensureServingAccess进行访问控制 - 若检查失败则直接中断流程,防止敏感文件泄露
- 顺着看
ensureServingAccess具体实现方法:
ts
export function ensureServingAccess(
url: string,
server: ViteDevServer,
res: ServerResponse,
next: Connect.NextFunction,
): boolean {
if (isFileServingAllowed(url, server)) {
return true // 允许访问
}
// ... 否则返回 403
}
- 所以真正决定是否允许访问的是:
isFileServingAllowed
核心权限判断:isFileServingAllowed
ts
export function isFileLoadingAllowed(
config: ResolvedConfig,
filePath: string,
): boolean {
const { fs } = config.server
if (!fs.strict) return true
if (config.fsDenyGlob(filePath)) return false
if (config.safeModulePaths.has(filePath)) return true
if (fs.allow.some((uri) => isUriInFilePath(uri, filePath))) return true
return false
}
- 它把
url(如 /.env/.)转换成 filePath - 转换函数是:
fsPathFromUrl(url) - 然后传给
isFileLoadingAllowed做最终判断 config.fsDenyGlob(filePath)使用picomatch进行模式匹配。
fsDenyGlob 的生成:动态匹配函数
通过 IDE 全局搜索发现,发现一共就俩个地方使用,另一个就是packages/vite/src/node/config.ts

ts
fsDenyGlob: picomatch(
// matchBase: true does not work as it's documented
// https://github.com/micromatch/picomatch/issues/89
// convert patterns without `/` on our side for now
server.fs.deny.map((pattern) =>
pattern.includes('/') ? pattern : `**/${pattern}`,
),
{
matchBase: false,
nocase: true,
dot: true,
},
- 使用
picomatch库,对于不含/的模式会转换为**/pattern - 所有不包含
/的模式(如.env)会被自动前缀为**/,变成**/.env
回头看fsPathFromUrl 方法
查询文件发现是通告导入包进来的
ts
import {
fsPathFromId,
fsPathFromUrl,
isFileReadable,
isImportRequest,
isInternalRequest,
isParentDirectory,
isSameFileUri,
normalizePath,
removeLeadingSlash,
urlRE,
} from '../../utils'
- 也就是
packages/vite/src/node/utils.ts中,查询fsPathFromUrl函数
ts
export function fsPathFromUrl(url: string): string {
return fsPathFromId(cleanUrl(url))
}
export function fsPathFromId(id: string): string {
const fsPath = normalizePath(
id.startsWith(FS_PREFIX) ? id.slice(FS_PREFIX.length) : id,
)
return fsPath[0] === '/' || VOLUME_RE.test(fsPath) ? fsPath : `/${fsPath}`
}
- 路径会被
normalizePath()处理,使用path.posix.normalize()。
ts
export function normalizePath(id: string): string {
return path.posix.normalize(isWindows ? slash(id) : id)
}
- 使用
path.posix.normalize()对路径进行标准化 - 在 Windows 上会先转为正斜杠
/ - 但 不会移除末尾的
/.或/..中的点目录
总结:漏洞成因链
bash
用户请求 → /.env/.
↓
path.resolve() → 解析为本地路径
↓
fsPathFromUrl() → 转换为文件系统路径
↓
normalizePath() → 得到 "/.env/."(未清理末尾 .)
↓
config.fsDenyGlob("/.env/.") → picomatch('**/.env') 不匹配
↓
isFileLoadingAllowed → 返回 true(误判为合法)
↓
Vite 尝试读取 "/.env/." → 实际访问 .env 内容或返回 403
0x4 修复建议
修复方案
- 升级到安全版本:
6.3.4、6.2.7、6.1.6、5.4.19 和 4.5.14版本中已修复 - 临时缓解措施:
部署 Web 应用防火墙(WAF)监控异常请求
server.fs.allow配置仅包含必要的目录
开发服务器不应暴露在公网
轮换API、数据库密钥
免责声明:本文仅用于安全研究目的,未经授权不得用于非法渗透测试活动。