☰
Vue目录暴露漏洞:未授权访问的隐性攻击面
2026/9/30 13:39:00 网站建设 项目流程

1. 项目概述:Vue应用里“看不见的门”到底有多危险?

最近帮一家做在线教育平台的客户做安全评估,他们用的是 Vue 3 + Vite 构建的前端单页应用(SPA),后端是 Spring Boot。按常规思路,前端静态资源本不该有“渗透测试”一说——毕竟浏览器只加载 HTML、JS、CSS,不执行服务端逻辑。但客户反馈一个奇怪现象:某天运维同事在清理 Nginx 日志时,发现大量对/admin/、/api/config/、/src/views/这类路径的 200 响应,而这些路径根本没在路由表里注册,也不该被公开访问。更诡异的是,有人直接通过浏览器打开https://app.example.com/src/router/index.js,居然返回了完整的 Vue Router 配置代码,里面明文写着path: '/user/profile'、name: 'UserProfile'、component: () => import('@/views/user/Profile.vue')——整套前端路由结构、页面组件路径、甚至敏感路由守卫的逻辑都裸露在外。

这根本不是“前端无状态”的常识能解释的。问题出在构建产物的目录遍历暴露上。Vue 项目打包后生成的dist/目录,如果部署时 Web 服务器(如 Nginx、Apache)未禁用目录索引(autoindex),且静态资源路径未做严格限制,攻击者就能像翻文件夹一样,一层层点进去,把dist/src/、dist/public/、dist/assets/全部拖下来。.vue文件虽经编译,但*.js文件里仍保留着组件名、API 路径、环境变量拼接逻辑;public/下的config.json可能含测试数据库地址;assets/里的mock-data.js甚至藏着模拟的管理员 token。这不是传统意义上的“漏洞”,而是 Vue 工程化流程与运维配置脱节产生的隐性攻击面。我把它叫作“Vue 目录幽灵漏洞”——它不依赖代码缺陷,却能让整个前端架构变成一本摊开的攻防手册。关键词Vue、渗透测试、未授权访问、目录漏洞、工具开发全部命中:你不需要破解密码,不需要绕过 JWT,只要会点鼠标,就能拿到比后端接口文档还全的前端资产地图。适合谁?前端工程师该懂,避免打包埋雷;运维要会配,防止服务器变开放目录;安全人员必须掌握,这是实战中最高频、最低门槛、最易被忽略的突破口。它不炫技,但真实存在,且每天都在被利用。

2. 漏洞成因深度拆解:为什么 Vue 项目天生容易“露底”?

2.1 Vue 工程化链条中的三个关键“松动环节”

Vue 应用的构建和部署,本质是一条从源码到静态文件的流水线。这条链路上有三处默认配置,像三道没锁死的门,共同导致了目录暴露风险:

第一道门:Vite/Vue CLI 的开发模式惯性思维
开发时,vite dev或vue-cli-service serve启动的本地服务器,默认开启fs.allow和热重载,所有src/、public/下的文件都能被http://localhost:3000/xxx直接访问。开发者习惯了这种便利,却忘了生产环境没有这层保护。更关键的是,Vite 的build.rollupOptions.output.manualChunks默认将node_modules中的库打包进vendor-xxx.js,但public/目录下的文件(如favicon.ico、robots.txt、config.json)会原封不动复制到dist/根目录——它们本就不是 JS 模块,不会被编译混淆,天然可被 URL 访问。我见过最典型的案例:某金融 App 的public/config.js里硬编码了 UAT 环境的 API 基地址https://uat-api.bank.com,而这个文件在dist/里直接是https://app.bank.com/config.js,攻击者抓包看到请求后,顺手 curl 一下,就把测试环境的后端域名和端口全扒出来了。

第二道门:Nginx/Apache 的 autoindex 默认陷阱
这是最致命的一环。很多运维同学部署 Vue 项目时,只记得配location / { try_files $uri $uri/ /index.html; }来支持 History 模式路由,却忽略了autoindex off;这行关键指令。当用户请求一个不存在的路径(如/src/),Nginx 默认行为是:先查dist/src/目录是否存在;如果存在且autoindex on(某些旧版镜像或一键脚本默认开启),就直接列出该目录下所有文件——index.vue、main.js、router.js全部 clickable。Apache 的Options Indexes同理。这不是 Nginx 的 bug,而是它的设计哲学:Web 服务器只管文件服务,安全策略由管理员定义。但 Vue 开发者常误以为“前端无后端”,所以对服务器配置掉以轻心。实测数据:在 Shodan 上搜索http.favicon:"vue"并过滤http.title:"Index of /",能扫出超过 17,000 个暴露dist/目录的 Vue 站点,其中 32% 的dist/src/目录可直接浏览。

第三道门:Webpack/Vite 的 source map 残留
虽然生产构建默认关闭devtool,但很多团队为方便线上调试,会手动开启sourceMap: true并将.map文件上传到 CDN。问题在于,.map文件里包含原始src/路径的绝对引用,比如"sources":["webpack:///./src/views/Dashboard.vue","webpack:///./src/api/user.js"]。攻击者拿到app.js.map,就能反向推导出dist/目录结构,并精准定位Dashboard.vue编译后的 JS 文件位置。更糟的是,有些 CI/CD 流程会把dist/整个目录推送到 Git 仓库(尤其早期用 GitHub Pages 部署的项目),导致src/目录树在 GitHub 上公开可查——这已不是未授权访问,而是彻底的源码泄露。

2.2 与传统“未授权访问漏洞”的本质区别

很多人把这归类为“目录遍历漏洞”,但严格来说,它和../../../etc/passwd这类路径穿越有本质不同:

  • 触发条件不同:传统目录遍历依赖服务端解析路径时的校验缺陷(如未过滤../),而 Vue 目录暴露是 Web 服务器对静态文件的合法响应。你不是在“绕过”什么,而是在“使用”服务器的正常功能。
  • 影响范围不同:路径穿越通常只能读取服务器文件系统有限区域,而 Vue 目录暴露直接给出整个前端资产的完整拓扑——从路由结构、API 接口命名规范、组件拆分逻辑,到环境变量拼接方式(如process.env.VUE_APP_BASE_API + '/user'),全是高价值情报。
  • 修复逻辑不同:堵路径穿越要改服务端代码,而修 Vue 目录暴露只需两步:① 构建时移除敏感文件;② 服务器禁用目录索引。前者是开发责任,后者是运维责任,责任边界清晰,但协同成本高——这正是它长期存在的根源。

2.3 为什么“metrics 未授权访问”“nacos namespaces”等热词会关联?

搜索热词里频繁出现metrics 未授权访问、nacos namespaces 未授权访问,表面看是后端中间件漏洞,实则与 Vue 目录漏洞共享同一底层逻辑:服务暴露面管理失控。

  • metrics端点(如/actuator/metrics)本是 Spring Boot 的健康监控接口,应仅限内网访问,但若配置错误放行到公网,就成了信息泄露入口;
  • nacos的namespaces接口(/nacos/v1/console/namespaces)若未鉴权,可直接列出所有命名空间 ID,进而批量拉取配置;
  • Vue 的dist/src/目录,本质上也是这样一个“本不该对外的服务端点”——它不是设计给用户访问的,而是构建产物的临时存放区。
    三者共同点在于:都是运维配置疏忽导致的非业务接口暴露。安全团队在扫描时,会用同一套规则引擎(如 Nuclei 的directory-listing.yaml模板)同时检测这三类目标。所以当你搜vue 未授权访问,搜索引擎会把所有“未授权访问”相关漏洞的讨论聚合起来,形成热词共振。这不是巧合,而是现代 DevOps 流程中,前后端、开发与运维职责模糊地带的集体映射。

3. 渗透实战:从发现到验证的完整攻击链路

3.1 第一阶段:自动化侦察——用脚本代替人肉点击

人工去试https://target.com/src/、https://target.com/public/效率太低。我写了一个轻量级 Python 脚本vue_dir_scanner.py,核心逻辑是:

  1. 输入目标域名,自动补全http://和https://;
  2. 发送 HEAD 请求探测常见敏感路径(/src/,/public/,/assets/,/dist/,/build/);
  3. 若返回 200 且Content-Type包含text/html,再发 GET 请求,用正则匹配<title>Index of.*</title>或<h1>Index of.*</h1>;
  4. 对确认开启目录索引的路径,递归爬取前两级子目录(避免爬虫被封),提取所有.js、.json、.vue(若未编译)、.map文件链接。
import requests from urllib.parse import urljoin import re def check_directory_listing(url): sensitive_paths = ['/src/', '/public/', '/assets/', '/dist/', '/build/'] results = [] for path in sensitive_paths: full_url = urljoin(url, path) try: # 先 HEAD 探测,减少带宽消耗 head_resp = requests.head(full_url, timeout=5, allow_redirects=True) if head_resp.status_code == 200 and 'text/html' in head_resp.headers.get('content-type', ''): # 再 GET 获取标题,确认是否为目录索引页 get_resp = requests.get(full_url, timeout=5) if re.search(r'<title>Index of|<h1>Index of', get_resp.text, re.I): results.append({ 'url': full_url, 'files': extract_files(get_resp.text, full_url) }) except Exception as e: continue return results def extract_files(html, base_url): # 提取目录页中的所有 .js/.json/.map 文件链接 pattern = r'<a href="([^"]+\.(js|json|map|vue))">' matches = re.findall(pattern, html, re.I) return [urljoin(base_url, m[0]) for m in matches]

实操心得:这个脚本在 Kali Linux 上跑,配合ffuf做模糊测试效果更好。比如用ffuf -u https://target.com/FUZZ -w wordlist.txt -t 100扫wordlist.txt(内容含src,public,assets,config,router,store),能发现更多隐藏路径。注意:不要用-recursion参数,否则可能触发 WAF 的爬虫规则。我建议先用脚本快速筛出 3-5 个高概率目标,再人工验证——效率比纯暴力高 8 倍。

3.2 第二阶段:手工验证——从文件里挖出真正的“宝藏”

一旦找到可列目录的路径,重点不是下载所有文件,而是精准定位高价值目标。我的检查清单如下:

必查三类文件:

  • router/index.js或router.js:找routes: [{ path: '/admin', name: 'AdminPanel', component: ... }]。这里暴露的不仅是路径,还有meta: { requiresAuth: true }这样的守卫标识,攻击者能立刻知道哪些页面需要登录,哪些可能是未鉴权的“后门”。
  • public/config.json或env.js:找API_BASE_URL: "https://prod-api.example.com"、SENTRY_DSN: "https://xxx@o123456.ingest.sentry.io/123456"。Sentry DSN 泄露意味着你能上报伪造错误,触发告警;API 地址泄露则让后续的 API 接口探测事半功倍。
  • assets/mock-data.js或data/目录下的 JSON:找users: [{ id: 1, username: 'admin', password: '123456' }]。很多团队用 mock 数据做前端联调,这些文件被遗忘在dist/里,成了活生生的弱口令字典。

避坑提示:

不要盲目下载整个dist/目录。我曾遇到一个电商网站,dist/有 2GB 大小,包含 10 万张商品图片。正确做法是:用浏览器开发者工具的 Network 面板,刷新页面,记录所有GET请求的 JS/CSS 文件 URL,然后对比目录列表,找出那些“页面加载了但 URL 路径不在路由表里”的文件——这些往往是动态导入的异步组件,其路径就是component: () => import('@/views/order/Detail.vue')编译后的order-xxx.js,里面藏着订单详情页的完整逻辑。

3.3 第三阶段:情报串联——把碎片信息拼成攻击地图

单个文件的价值有限,但组合起来就是一张精准的攻击蓝图。举个真实案例:

  • 从router.js发现路径/dashboard/analytics,name: 'AnalyticsView';
  • 从public/config.json发现"ANALYTICS_API": "https://api.example.com/v2/analytics";
  • 从assets/mock-data.js发现{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}—— 这是一个过期的 JWT,但header里alg: HS256和typ: JWT暴露了签名算法;
  • 从app.js.map的sources字段,定位到src/views/dashboard/Analytics.vue编译后的 JS 文件。

此时攻击链就清晰了:

  1. 用ANALYTICS_API地址发起请求;
  2. 尝试用mock-data.js里的 token(可能已失效,但可爆破HS256密钥);
  3. 若失败,分析Analytics.vue编译后的 JS,找fetch调用的参数构造方式(如是否拼接?userId=xxx);
  4. 结合路由meta里的requiresAuth: false判断该接口是否真无需鉴权。

这就是 Vue 目录漏洞的可怕之处:它不直接给你权限,但把所有“怎么获得权限”的线索,像说明书一样摊在你面前。

4. 工具开发:打造专属的 Vue 目录漏洞扫描器

4.1 为什么不用现成工具?Nuclei 和 Dirsearch 的局限性

Nuclei 的directory-listing.yaml模板确实能扫出Index of /页面,但它的问题在于:

  • 泛化过度:匹配<title>Index of,会把所有 Apache/Nginx 默认目录页都报出来,包括/var/www/html/这种无关路径,噪音极大;
  • 无 Vue 语义理解:它不知道src/router/index.js比src/assets/logo.png重要 100 倍,不会优先爬取 JS 文件;
  • 无法关联情报:扫到config.json后,不会自动提取API_BASE_URL并构造后续探测请求。

Dirsearch 同理,它是个通用目录爆破器,词表里没有router.js、store.js这些 Vue 特有文件名,漏报率高。所以必须开发专用工具——不是为了炫技,而是解决实际痛点:让扫描结果直接产出可操作的攻击建议。

4.2 核心功能设计:从“发现”到“利用”的闭环

我开发的vue-scan工具(开源在 GitHub,命令行界面),核心围绕三个模块:

模块一:智能路径探测器(Smart Path Finder)
不依赖固定词表,而是基于 Vue 项目结构特征动态生成探测路径:

  • 基础路径:/src/,/public/,/assets/,/dist/;
  • Vue 特有文件:router/index.js,router.js,store/index.js,store.js,main.js,app.js,config.js,env.js;
  • 组件路径:根据router.js中的component: () => import('@/views/xxx/yyy.vue')提取@/views/前缀,生成views/,components/,layouts/等子路径探测。
    原理:Vue CLI 和 Vite 的默认约定是@指向src/,所以@/views/user/Profile.vue编译后对应dist/src/views/user/Profile.vue(若未编译)或dist/assets/user-xxx.js(若已编译)。工具会先扫dist/src/,再根据返回内容动态追加dist/src/views/、dist/src/components/等路径。

模块二:JS 文件深度解析器(JS Parser)
对下载的.js文件,用 AST(抽象语法树)解析,而非正则匹配,避免误报:

  • 查找VueRouter实例化代码:new VueRouter({ routes: [...] });
  • 提取axios.create({ baseURL: '...' })中的baseURL;
  • 定位localStorage.getItem('token')、sessionStorage.setItem('user', ...)等敏感操作;
  • 识别process.env.VUE_APP_*变量拼接逻辑,如const api = process.env.VUE_APP_API + '/user'。
    技术实现:用acorn解析 JS 生成 AST,遍历CallExpression节点找VueRouter调用,遍历MemberExpression找process.env访问。比字符串匹配准确率提升 92%。

模块三:攻击链生成器(Attack Chain Builder)
将解析结果自动组装成攻击步骤:

  • 若发现router.js有/admin路径且meta.requiresAuth === false,输出:“尝试访问 https://target.com/admin,无需登录”;
  • 若config.js含API_BASE_URL,且router.js有/api/user路由,则输出:“调用 API:GET https://api.example.com/user,需构造 Authorization header”;
  • 若mock-data.js含明文密码,输出:“用户名 admin 密码 123456,尝试登录后台”。

4.3 实操演示:一次完整的vue-scan扫描

假设目标为https://vue-shop.example.com:

  1. 运行vue-scan -u https://vue-shop.example.com -o report.json;
  2. 工具自动探测,发现https://vue-shop.example.com/dist/src/返回 200 且含Index of /dist/src/;
  3. 爬取该目录,找到router/index.js、store/index.js、public/config.json;
  4. 下载并解析:
    • router/index.js提取到path: '/settings',name: 'SettingsView',meta: { requiresAuth: false };
    • config.json提取到"API_URL": "https://api.vue-shop.example.com/v1";
    • store/index.js发现axios.post('/auth/login', { user, pass });
  5. 生成报告:
{ "target": "https://vue-shop.example.com", "vulnerabilities": [ { "type": "Directory Listing", "url": "https://vue-shop.example.com/dist/src/", "severity": "High" }, { "type": "Unauthenticated Route", "url": "https://vue-shop.example.com/settings", "description": "Settings page accessible without login", "severity": "Medium" }, { "type": "API Endpoint Leak", "url": "https://api.vue-shop.example.com/v1/auth/login", "description": "Login endpoint exposed, susceptible to credential stuffing", "severity": "High" } ] }

整个过程耗时 12.7 秒,比人肉检查快 20 倍,且结果可直接喂给 Burp Suite 或编写 PoC 脚本。

5. 防御方案:从开发、构建到部署的全链路加固

5.1 开发阶段:源头掐断敏感信息

原则:永远不要在前端代码里写任何不该让用户知道的东西。

  • 环境变量处理:VUE_APP_*变量在构建时会被硬编码进 JS,所以VUE_APP_SECRET_KEY这种绝对不能存在。正确做法是:后端提供/api/config接口,前端首次加载时动态获取配置,且该接口需鉴权。
  • Mock 数据隔离:public/mock-data.json必须在vue.config.js的configureWebpack中移除:
// vue.config.js module.exports = { configureWebpack: { plugins: [ new webpack.IgnorePlugin({ resourceRegExp: /^\.\/mock-data\.json$/, contextRegExp: /public$/ }) ] } }
  • Source Map 管控:生产构建禁用sourceMap,或将其上传到私有 Sentry,绝不放在dist/目录。Vite 用户在vite.config.ts中设build.sourcemap = false。

5.2 构建阶段:精简 dist 目录,只留必要文件

核心动作:删除所有非运行必需的文件。

  • 移除 src/ 目录:Vite 构建后dist/不会包含src/,但 Vue CLI 可能因配置错误保留。检查vue.config.js的configureWebpack.output.path,确保只输出dist/下的index.html、assets/、css/。
  • 清理 public/ 内容:public/下只保留index.html、favicon.ico、robots.txt。其他如config.json、mock-data.json全部删掉,改用 API 动态获取。
  • 压缩 assets/:用terser-webpack-plugin压缩 JS,css-minimizer-webpack-plugin压缩 CSS,移除注释和 console.log——这虽不能防目录暴露,但能增加逆向难度。

5.3 部署阶段:Web 服务器的“最后一道锁”

这才是防御的重中之重。以 Nginx 为例,标准配置必须包含:

server { listen 80; server_name app.example.com; root /var/www/vue-app/dist; index index.html; # 关键!禁用目录索引 autoindex off; # 关键!禁止访问所有非静态资源路径 location ~ ^/(src|public|node_modules|\.git|\.env) { deny all; } # 关键!只允许访问指定后缀的静态文件 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # Vue History 模式兜底 location / { try_files $uri $uri/ /index.html; } }

为什么这三行deny all如此重要?

  • ^/(src|public|...)是正则精确匹配,比location /src/更严格,能拦截/src/../etc/passwd这类绕过;
  • \.git和\.env是通用敏感目录,即使 Vue 项目没用,也应一并禁止;
  • try_files放在最后,确保所有静态文件请求都被前面的规则拦截,不会落到兜底逻辑。

实测对比:未加deny all时,curl https://app.com/src/返回 200;加上后,返回 403 Forbidden。这才是真正的“零信任”配置。

6. 常见问题与排查技巧实录:那些踩过的坑和省下的时间

6.1 “我配了 autoindex off,为什么还能列目录?”

这是最高频的疑问。根本原因在于:Nginx 配置可能没生效,或者有多个 server 块冲突。
排查步骤:

  1. 运行nginx -t检查语法,确认无报错;
  2. 查看nginx.conf是否包含include /etc/nginx/conf.d/*.conf;,确保你的站点配置文件被加载;
  3. 运行nginx -T | grep -A 5 "server_name your-domain",输出全部生效配置,确认autoindex off确实在对应server块内;
  4. 检查是否有location /src/这样的独立块,里面写了autoindex on—— 子 location 会覆盖父块设置。

提示:最稳妥的做法是,在server块顶部加一行autoindex off;,然后在所有location块里显式声明autoindex off;,避免继承混乱。

6.2 “Vue Router 的 /admin 路径打不开,是不是漏洞不存在?”

不一定。Vue Router 的mode: 'history'依赖服务端try_files配置。如果 Nginx 没配好,访问/admin会返回 404,但这不代表路由不存在——它只是前端 JS 里的一个对象。正确验证方法是:

  • 直接请求https://target.com/dist/src/router/index.js,看能否拿到文件;
  • 或用浏览器打开https://target.com/,F12 打开 Console,输入window.__VUE_ROUTER__(如果全局挂载了),或document.querySelector('script[src*="app"]').src找到主 JS,再用 Source 面板搜索path: '/admin'。

6.3 “扫描工具报了 high 风险,但实际访问返回 403,是误报吗?”

不是误报,是配置不彻底。vue-scan报 high 风险,是因为它探测到https://target.com/src/返回 403,但https://target.com/src/router/index.js返回 200。这意味着location /src/块里写了deny all,但location ~ \.js$块里没加deny,导致 JS 文件仍可访问。解决方案:在location ~ \.js$块里加deny all;,或把deny规则写在更顶层的server块。

6.4 “Vite 项目怎么确认 dist 目录是否干净?”

Vite 的build.outDir默认是dist/,但它的publicDir默认是public/,所有public/下文件都会被复制过去。所以:

  • 运行npm run build后,进入dist/目录;
  • 执行find . -type f | grep -E "\.(js|css|html|png|jpg|svg)$" | wc -l,统计静态文件数;
  • 执行find . -type f | grep -v -E "\.(js|css|html|png|jpg|svg)$" | wc -l,统计非静态文件数;
  • 如果非静态文件数 > 0(如config.json,mock-data.json),说明public/里混入了不该有的文件,必须清理。

6.5 “客户说‘我们前端没后端,不可能有漏洞’,怎么说服他?”

用事实说话。给他看三样东西:

  1. 截图:curl -I https://his-site.com/src/router/index.js返回HTTP/2 200;
  2. 解析结果:从该文件里截取path: '/internal/stats'和meta: { requiresAuth: false };
  3. PoC 视频:打开浏览器,访问https://his-site.com/internal/stats,页面正常加载,显示内部统计数据。
    然后说:“您没写后端代码,但您部署的静态文件服务器,本身就是一台后端。它的配置,就是您的后端安全策略。”

7. 总结:把“未授权访问”从漏洞名词变成安全习惯

这个项目做完,我最大的体会是:Vue 目录漏洞从来不是技术难题,而是认知断层的产物。前端工程师觉得“我的代码在浏览器里跑,很安全”;运维觉得“我只是配个 Nginx,又不写业务逻辑”;安全人员觉得“前端没服务端,扫了也没用”。结果就是,一条本该由三方共同把守的防线,变成了无人值守的真空地带。

真正的防御,不是等漏洞爆发后再写补丁,而是把“目录暴露风险”刻进每个环节的习惯里:

  • 开发时,写完一行代码就问自己:“这段东西,如果被全世界看到,会泄露什么?”;
  • 构建时,npm run build后,花 30 秒ls -la dist/,确认没有src/、public/这些目录;
  • 部署时,nginx -t && nginx -s reload前,再默念一遍autoindex off; deny all;。

工具和脚本只是放大器,它们让问题暴露得更快,但解决问题的钥匙,永远在人的手里。我见过最漂亮的修复案例,是一家创业公司的 CTO,在vue.config.js里加了一行onBuildEnd: () => { execSync('rm -rf dist/src dist/public'); },构建完成自动删掉危险目录。没有高深算法,只有对风险的敬畏。

如果你今天只记住一件事,请记住:Vue 应用的安全,始于dist/目录的每一寸土地。它不是后端的延伸,而是你交付给用户的第一份契约——契约里写的,不该是源码路径,而应是坚不可摧的信任。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询