vite安全漏洞deny解决方案

背景

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这种,就比较麻烦了,一样的,如果是小版本都无所谓,但是如果是大版本,那你就需要升级宿主依赖,这种改动量就更大了。

升级后的自查方式

  1. 本地build不同环境的产物 无报错

  2. 本地dev --host打开开发服务器 能正常访问 无报错

  3. 本地打开 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>
  `;
}

如果有问题,自己稍微调整一下就行了。

相关推荐
CodeCraft Studio4 分钟前
Excel处理控件Spire.XLS系列教程:C# 合并、或取消合并 Excel 单元格
前端·c#·excel
头顶秃成一缕光15 分钟前
若依——基于AI+若依框架的实战项目(实战篇(下))
java·前端·vue.js·elementui·aigc
冴羽yayujs18 分钟前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·vue.js·前端框架·react
木木黄木木30 分钟前
HTML5图片裁剪工具实现详解
前端·html·html5
念九_ysl33 分钟前
基数排序算法解析与TypeScript实现
前端·算法·typescript·排序算法
海石33 分钟前
vue2升级vue3踩坑——【依赖注入】可能成功了,但【依赖注入】成功了不太可能
前端·vue.js·响应式设计
uhakadotcom1 小时前
Vite 与传统 Bundler(如 Webpack)在 Node.js 应用的性能对比
前端·javascript·面试
uhakadotcom1 小时前
Socket.IO 简明教程:实时通信的基础知识
前端·javascript·面试
机器视觉知识推荐、就业指导1 小时前
QML 批量创建模块 【Repeater】 组件详解
前端·c++·qml