背景
github.com/vitejs/vite... www.huaweicloud.com/notice/2025...
原因
遇到问题先找原因,在vite的dev环境中,会对css或者其他文件格式的请求添加一个?import的标识符表示这个模块需要经过transform的中间件进行转换,将他们装换成es的模块,而vite对哪些文件访问或者请求是合法的是通过serveRawFsMiddleware的中间件进行处理,在vite的devServer中,transformMiddleware位置是在serveRawFsMiddleware之前的,所以如果在transformMiddleware中将这个请求直接req.end()调了后,实际上后续的中间件就无法拦截了,这就会导致一个问题,我们--host起的局域网的ip地址后,访问localhost:3000/@fs/xx/xx.xx?import这样的请求,实际上应该是先经过deny或者allow进行鉴权处理后再交给transform进行转换,或者像已经解决的版本在transform中额外做一层拦截
如果是不合法的请求直接返回403即可。
影响面
实际上这个漏洞对于开发者来说影响面不大,因为devServer只会在开发环境使用,生成你都上了nginx经过waf了肯定跟本地服务器没啥关系了,但是会存在一个问题,在同一个局域网内,大家起的都是--host,那么我就可以直接通过拼你ip的方式去访问你电脑上的其他文件,比如var/或者其他的目录,这个就有点恐怖了,虽然按照我的理解,内网里面如果本身没有安全防护可以互相ping ip的话其实也可能会有访问漏洞,但是。。。。谁知道呢。
解决方法
分两种把,一种是你就硬升级,其实如果是中间版本的升级或者是patch版本的升级都还好,怕就怕你是major版本的升级,如果是2->4这种,你是无法评估出来具体的影响范围的,毕竟生产构建的rollup也会随着vite升级,同时如果你是类似uni-app或者nuxt这种,就比较麻烦了,一样的,如果是小版本都无所谓,但是如果是大版本,那你就需要升级宿主依赖,这种改动量就更大了。
升级后的自查方式
-
本地build不同环境的产物 无报错
-
本地dev --host打开开发服务器 能正常访问 无报错
-
本地打开 http://localhost:{端口号}/@fs/var/log/daily.out?import&raw
a. 如果出现的不是403页面表示你这个vite版本依旧是有问题的,请自行检查升级
b. 如果出现的是403页面表示无问题
或者还有一种方式,咱们不去动vite版本,我们也写一个中间件,去拦截不合法的请求,相当于将这个动作继续前置,插件代码已经测试过了,对于2~6版本理论上都没问题
typescript
// vite-fs-security-plugin.ts
import path from 'node:path';
import type { Plugin, ViteDevServer } from 'vite';
import { normalizePath } from 'vite';
import { trailingQuerySeparatorsRE,cleanUrl,renderRestrictedErrorHTML, fsPathFromUrl, isParentDirectory } from './utils';
export function fsSecurityPlugin(): Plugin {
return {
name: 'vite:fs-security-patch',
configureServer(server) {
// 在所有中间件之前添加我们的安全检查中间件
server.middlewares.use((req, res, next) => {
if (req.url) {
const url = decodeURI(req.url!);
const urlWithoutTrailingQuerySeparators = url.replace(
trailingQuerySeparatorsRE,
'',
);
// 检查是否带有?import参数并且是绝对路径
if (url && url.includes('?import') &&
!isFileServingAllowed(urlWithoutTrailingQuerySeparators, server)) {
// 检查是否访问的是可能的系统路径
const rawPath = cleanUrl(decodeURIComponent(url));
const normalizedPath = normalizePath(path.resolve(server.config.root, rawPath.slice(1)));
// 记录安全事件
server.config.logger.error(`安全警告: 尝试访问限制路径: ${normalizedPath}`);
// 返回403禁止访问
res.statusCode = 403;
res.write(renderRestrictedErrorHTML());
res.end();
return;
}
next();
} else {
next();
}
});
}
};
}
export function isFileServingAllowed(
url: string,
server: ViteDevServer
): boolean {
if (!server.config.server.fs.strict) return true;
const file = fsPathFromUrl(url);
// 下一行代码不校验ts类型
// @ts-ignore
if(server._fsDenyGlob){
// @ts-ignore
if (server._fsDenyGlob(file)) return false;
}
// @ts-ignore
if(server.fsDenyGlob){
// @ts-ignore
if (server.fsDenyGlob(file)) return false;
}
if (server.moduleGraph.safeModulesPath.has(file)) return true;
if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file)))
return true;
return false;
}
utils.ts
typescript
// 单独给vite4.0以下版本兼容处理
import { normalizePath } from 'vite';
export const postfixRE = /[?#].*$/;
export const FS_PREFIX = '/@fs/';
export const VOLUME_RE = /^[A-Z]:/i;
export const trailingQuerySeparatorsRE = /[?&]+$/;
// 清除url中的查询参数和hash
export function cleanUrl(url: string): string {
return url.replace(postfixRE, '');
}
// 给路径添加尾部斜杠
export function withTrailingSlash(path:string){
if(path[path.length-1] !=='/'){
return `${path}/`;
}else{
return path;
}
}
// 判断file是否是dir的子目录
export function isParentDirectory(dir:string,file:string):boolean{
dir = withTrailingSlash(dir);
return(
file.startsWith(dir) || file.toLowerCase().startsWith(dir.toLowerCase())
);
}
// 从url中获取文件系统路径
export function fsPathFromUrl(url:string):string{
return fsPathFromId(cleanUrl(url));
}
// 从id中获取文件系统路径
export function fsPathFromId(id:string):string{
const fsPath = normalizePath(
id.startsWith(FS_PREFIX) ? id.slice(FS_PREFIX.length) : id
);
return fsPath.startsWith('/') || fsPath.match(VOLUME_RE) ? fsPath : `/${fsPath}`;
}
// 渲染403错误页面
export function renderRestrictedErrorHTML(): string {
// to have syntax highlighting and autocompletion in IDE
const html = String.raw;
return html`
<body>
<h1>403 Restricted</h1>
<style>
body {
padding: 1em 2em;
}
</style>
</body>
`;
}
如果有问题,自己稍微调整一下就行了。