Element Plus Notification组件XSS漏洞深度解析与安全修复实践

Element Plus Notification组件XSS漏洞深度解析与安全修复实践 1. 项目概述一次由社区反馈驱动的安全审计最近在维护一个基于 Vue 3 和 Element Plus 的前端项目时我收到了一条来自安全团队的内部提醒大意是说我们项目中使用的某个 UI 组件可能存在潜在的 XSS跨站脚本攻击风险。这个提醒立刻引起了我的警觉毕竟前端安全无小事任何一点疏忽都可能导致用户数据泄露甚至更严重的后果。经过一番排查问题最终定位到了我们广泛使用的 Element Plus 2.9.0 版本中的 Notification通知组件。这个组件在特定配置下会直接将传入的message属性作为 HTML 进行渲染如果这个message来自不可信的用户输入就为 XSS 攻击打开了大门。这并非一个孤立的案例。在开源社区和各大企业的安全扫描中由富文本渲染不当引发的漏洞屡见不鲜。Element Plus 作为 Vue 生态中最主流的 UI 库之一其安全性直接影响着成千上万的线上应用。因此我决定深入这个“漏洞”不仅是为了修复我们自己的项目更是想彻底搞清楚它的成因、影响范围以及最稳妥的修复方案。这个过程就像一次小型的“数字法医”工作从漏洞现象出发逆向推导其源码实现最终找到并实施修复。接下来我将把这次完整的分析、复现和修复过程记录下来希望能为遇到类似问题的开发者提供一个清晰的解决路径。2. 漏洞原理深度剖析危险的dangerouslyUseHTMLString要理解这个漏洞我们首先得抛开“漏洞”这个略显惊悚的词回到前端开发一个非常基础的概念内容渲染的两种方式——纯文本与 HTML。2.1 核心风险参数dangerouslyUseHTMLString在 Element Plus 的 Notification 组件同样适用于 Message、MessageBox 等组件的 API 中有一个名为dangerouslyUseHTMLString的属性。顾名思义这个名字本身就充满了警告意味——“危险地使用 HTML 字符串”。它的默认值是false。当它为false时组件会将传入的message字符串作为纯文本处理任何 HTML 标签都会被转义成普通字符显示在页面上。例如输入scriptalert(‘xss’)/script页面上只会显示这段字符串本身而不会执行其中的脚本。然而当开发者将dangerouslyUseHTMLString设置为true时情况就完全不同了。此时组件会使用 Vue 的v-html指令或其底层等效方法来渲染message字符串。v-html的功能是将其绑定的值作为原始的 HTML 插入到 DOM 中。这意味着script标签会被浏览器解析并执行img src“x” onerror“alert(1)”也会触发onerror事件。为什么会有这个参数它的设计初衷是为了提供灵活性。有时我们确实需要在通知内容里加一些简单的样式比如加粗、换行或者插入一个图标。例如ElNotification({ title: ‘提示’, message: ‘操作strong成功/strong’, // 希望“成功”二字加粗 dangerouslyUseHTMLString: true, type: ‘success’ })在这种情况下开启这个选项是合理且必要的。2.2 漏洞触发场景与攻击向量那么漏洞究竟在哪里问题出在“不可信数据”与“危险开关”的结合上。在 2.9.0 版本及之前的某些实现中存在以下风险场景直接使用用户输入作为message这是最典型的场景。例如一个功能是将用户提交的评论内容实时用 Notification 提示出来。如果代码这样写// 假设 userInput 来自表单输入框 ElNotification({ message: userInput, // 用户可能输入 script窃取Cookie()/script dangerouslyUseHTMLString: true // 开发者可能为了格式而开启 })攻击者就可以在输入框中注入恶意脚本。间接数据污染有时message并非直接来自用户而是经过了一些后台接口的返回。如果后端没有对数据进行严格的过滤和转义或者接口本身存在安全风险那么攻击者可能通过攻击后端接口将恶意代码“存储”到数据库中。当前端从接口获取数据并渲染时漏洞同样会被触发。配置的误用与遗忘在一些历史代码或由不同开发者维护的模块中可能全局配置了dangerouslyUseHTMLString: true但后续开发者在使用时并未意识到其传递的message内容需要经过严格净化从而无意中引入了风险。攻击者能做什么一旦恶意脚本被执行攻击者可以窃取用户的登录凭证Cookie、LocalStorage 中的 Token。伪造用户界面进行钓鱼操作。发起未经用户同意的请求CSRF 攻击。甚至利用浏览器漏洞进行更深层次的攻击。这个漏洞的严重性在于它位于一个非常基础且高频使用的 UI 组件中影响面广且利用门槛相对较低。3. 漏洞复现与影响范围验证理论分析之后我们必须动手验证亲眼看到漏洞是如何被触发的这有助于我们评估其真实威胁。我搭建了一个最简单的 Vue 3 Element Plus 2.9.0 环境进行复现。3.1 环境搭建与基础代码首先创建一个 Vue 项目并安装指定版本的 Element Plusnpm create vuelatest my-vue-app cd my-vue-app npm install npm install element-plus2.9.0然后在main.js或main.ts中全局引入 Element Plusimport { createApp } from ‘vue’ import ElementPlus from ‘element-plus’ import ‘element-plus/dist/index.css’ import App from ‘./App.vue’ const app createApp(App) app.use(ElementPlus) app.mount(‘#app’)3.2 构造攻击Payload并复现在 App.vue 中我们编写一个触发漏洞的示例。为了模拟攻击我们构造几个典型的 XSS Payloadtemplate div h3Element Plus 2.9.0 Notification XSS 复现/h3 el-button click“triggerXSS1”触发基础Script XSS/el-button el-button click“triggerXSS2”触发Img onerror XSS/el-button el-button click“triggerXSS3”触发SVG XSS (更隐蔽)/el-button el-button click“safeNotification”安全用法纯文本/el-button /div /template script setup import { ElNotification } from ‘element-plus’ const triggerXSS1 () { // Payload 1: 直接执行脚本 const maliciousMessage scriptalert(‘XSS via script tag! ‘ document.cookie)/script // 注意现代浏览器可能对直接插入的script标签不执行但这是经典payload ElNotification({ title: ‘危险通知’, message: maliciousMessage, dangerouslyUseHTMLString: true, // 关键风险开关 type: ‘warning’, duration: 0 // 不自动关闭方便观察 }) } const triggerXSS2 () { // Payload 2: 利用图片加载错误 const maliciousMessage img src“x” onerror“alert(‘XSS via img onerror! ‘ localStorage.getItem(‘token’))” / ElNotification({ message: maliciousMessage, dangerouslyUseHTMLString: true, type: ‘error’ }) } const triggerXSS3 () { // Payload 3: 使用SVG标签可能绕过一些简单的过滤器 const maliciousMessage svg/onload“alert(‘XSS via SVG!’)” ElNotification({ message: maliciousMessage, dangerouslyUseHTMLString: true, type: ‘success’ }) } const safeNotification () { // 安全用法不使用危险开关或对内容进行转义 const userContent ‘用户输入scriptalert(“bad”)/script’ ElNotification({ message: userContent, // 即使包含HTML标签也会被显示为文本 // dangerouslyUseHTMLString: false, // 默认就是false type: ‘info’ }) } /script当你点击前三个按钮时如果页面成功弹出警告框并显示了 Cookie 或 LocalStorage 的模拟信息就证明了 XSS 漏洞确实存在且可以被利用。而点击第四个按钮你会看到script标签被原样显示为文本这就是安全的效果。注意在实际的渗透测试或安全研究中alert仅用于证明漏洞存在。真实的攻击载荷会隐蔽地发送数据到攻击者控制的服务器。3.3 影响范围评估这个漏洞的影响范围需要从两个维度看版本范围根据官方修复记录和代码分析此问题在 Element Plus2.9.0 及之前的所有版本中只要开发者启用了dangerouslyUseHTMLString并传入了未经验证的数据就可能存在风险。2.9.0 之后的版本如 2.9.1中官方对此进行了修复。组件范围不仅仅是 Notification 组件。Element Plus 中采用类似设计模式的组件都需要检查主要包括ElNotification(通知)ElMessage(消息提示)ElMessageBox(消息弹框)ElAlert(警告) – 当其description属性支持 HTML 时 它们的共同点是都有一个接受字符串内容、并可选支持 HTML 渲染的属性。4. 官方修复方案解读与源码对比在意识到问题后Element Plus 团队迅速响应并发布了修复。理解官方的修复思路比单纯地升级版本更有价值。我们可以通过对比修复前后的源码来学习。4.1 修复前的问题代码简化的逻辑示意在 2.9.0 版本中Notification 组件内部处理message的部分逻辑大致如下非完整源码为说明问题而简化// 伪代码示意风险点 const NotificationInstance { props: { message: { type: String, default: ‘’ }, dangerouslyUseHTMLString: { type: Boolean, default: false } }, setup(props) { // 渲染函数中 return () h(‘div’, { class: ‘el-notification__content’ }, [ props.dangerouslyUseHTMLString ? h(‘div’, { innerHTML: props.message }) // 风险行直接注入 innerHTML : h(‘div’, { class: ‘el-notification__message’ }, props.message) // 安全文本渲染 ]) } }关键风险在于当dangerouslyUseHTMLString为true时直接使用了innerHTML或 Vue 的v-html指令来设置元素的 HTML 内容没有对props.message进行任何过滤。4.2 修复后的安全实现在修复版本如 2.9.1中官方引入了一个关键的内部工具函数sanitizeHTML。修复后的逻辑变为// 伪代码展示修复逻辑 import { sanitizeHTML } from ‘../utils/sanitize-html’ // 引入HTML净化器 const NotificationInstance { props: { /* 同上 */ }, setup(props) { return () h(‘div’, { class: ‘el-notification__content’ }, [ props.dangerouslyUseHTMLString ? h(‘div’, { innerHTML: sanitizeHTML(props.message) }) // 修复行注入前先净化 : h(‘div’, { class: ‘el-notification__message’ }, props.message) ]) } }这个sanitizeHTML函数是整个修复的核心。它的作用是对输入的 HTML 字符串进行“消毒”移除或转义其中可能执行脚本的危险标签和属性。4.3sanitizeHTML函数的工作原理浅析虽然我们看不到 Element Plus 内部sanitizeHTML的全部实现可能基于成熟的库如dompurify但其基本原理是创建一个沙盒 DOM 元素例如一个不可见的div。将字符串设置为该元素的innerHTML浏览器会自动解析 HTML 结构。遍历 DOM 树检查每个节点元素、属性。应用白名单策略这是最关键的一步。只允许安全的标签如b,strong,i,em,span,p,br和安全的属性如style,class,># 查看当前版本 npm list element-plus # 升级到最新稳定版 npm update element-plus # 或指定升级到修复版本 npm install element-plus^2.9.1升级后必须做的检查回归测试全面测试项目中所有使用 Notification、Message 等组件的地方确保功能正常。特别是那些原本使用了dangerouslyUseHTMLString: true的地方检查其 HTML 渲染效果是否符合预期比如加粗、换行是否还在。API 变更检查查阅官方升级指南看是否有其他破坏性变更影响你的项目。5.2 策略二临时安全封装适用于无法立即升级的情况如果因种种原因无法立即升级可以创建一个安全的包装函数来替代直接调用ElNotification。// utils/safe-notification.js import { ElNotification } from ‘element-plus’ import DOMPurify from ‘dompurify’ // 需要安装这个库npm install dompurify /** * 安全的 Notification 调用 * param {Object} options - ElNotification 的配置项 * param {string} options.message - 消息内容 * param {boolean} [options.dangerouslyUseHTMLStringfalse] - 是否允许HTML * returns {Object} ElNotification 的返回值 */ export function safeNotification(options) { const config { …options } // 如果调用者显式要求使用HTML字符串则对message进行净化 if (config.dangerouslyUseHTMLString config.message) { // 使用 DOMPurify 进行HTML消毒 config.message DOMPurify.sanitize(config.message, { ALLOWED_TAGS: [‘b’, ‘strong’, ‘i’, ‘em’, ‘u’, ‘s’, ‘p’, ‘br’, ‘span’], // 自定义允许的标签 ALLOWED_ATTR: [‘style’, ‘class’, ‘data-*’] // 自定义允许的属性 }) // 注意净化后config.dangerouslyUseHTMLString 仍然为 trueElNotification会使用v-html渲染净化后的安全HTML。 } else { // 如果调用者没有要求HTML确保开关为false防御性编程 config.dangerouslyUseHTMLString false } return ElNotification(config) } // 在组件中使用 import { safeNotification } from ‘/utils/safe-notification’ safeNotification({ title: ‘安全提示’, message: ‘用户输入scriptalert(“xss”)/scriptb重要/b’, // 脚本会被过滤b会保留 dangerouslyUseHTMLString: true, type: ‘info’ })这种方案的优缺点优点能快速缓解风险无需升级整个库可控性强。缺点增加了维护成本需要确保项目内所有相关调用都改用这个安全函数不能有遗漏。这可以通过代码审查或 ESLint 自定义规则来辅助保证。5.3 策略三全局 Monkey Patch侵入性较强谨慎使用在应用入口处重写 Element Plus 提供的ElNotification方法为其注入净化逻辑。// main.js import { createApp } from ‘vue’ import ElementPlus from ‘element-plus’ import ‘element-plus/dist/index.css’ import App from ‘./App.vue’ import DOMPurify from ‘dompurify’ const app createApp(App) // 在 use ElementPlus 之前或之后进行Patch const originalNotify app.config.globalProperties.$notify // 或者直接导入的 ElNotification if (originalNotify) { const patchedNotify (options) { if (options.dangerouslyUseHTMLString options.message) { options.message DOMPurify.sanitize(options.message, { ALLOWED_TAGS: [‘b’, ‘i’, ‘strong’, ‘em’, ‘br’, ‘p’, ‘span’], ALLOWED_ATTR: [‘style’, ‘class’] }) } return originalNotify(options) } // 替换全局方法 app.config.globalProperties.$notify patchedNotify // 如果需要也可以替换直接导入的 ElNotification这更复杂可能需要代理整个模块 } app.use(ElementPlus) app.mount(‘#app’)警告Monkey Patch 是一种侵入性很强的技术可能会带来难以调试的副作用尤其是当 Element Plus 内部实现或 API 发生变化时。仅作为临时应急方案并尽快安排升级。5.4 修复后的验证测试无论采用哪种方案修复后都必须进行验证。单元测试为你的安全封装函数或补丁逻辑编写单元测试。// safe-notification.test.js import { safeNotification } from ‘./safe-notification’ import { describe, it, expect, vi } from ‘vitest’ // 或 jest // 模拟 ElNotification vi.mock(‘element-plus’, () ({ ElNotification: vi.fn() })) describe(‘safeNotification’, () { it(‘应该净化危险的HTML内容’, () { const dangerousMsg ‘scriptbad()/scriptbgood/b’ safeNotification({ message: dangerousMsg, dangerouslyUseHTMLString: true }) // 验证传递给 ElNotification 的 message 中不包含 script 标签 expect(ElNotification).toHaveBeenCalledWith( expect.objectContaining({ message: expect.not.stringContaining(‘script’), message: expect.stringContaining(‘bgood/b’) }) ) }) it(‘当不使用HTML时应强制关闭危险开关’, () { safeNotification({ message: ‘test’ }) expect(ElNotification).toHaveBeenCalledWith( expect.objectContaining({ dangerouslyUseHTMLString: false }) ) }) })手动渗透测试在修复后的应用上重新运行第 3 节中的漏洞复现代码确认攻击 Payload 不再生效脚本不执行标签被转义或移除。6. 前端安全开发规范与长效防御机制修复一个具体的漏洞是“治标”建立良好的安全开发习惯才是“治本”。针对 HTML 渲染风险团队应该形成以下规范6.1 安全编码红线默认禁止原则除非有非常明确且必要的富文本展示需求否则永远不要使用dangerouslyUseHTMLString、v-html或innerHTML。将其视为一条需要特批的“红线”。输入净化原则如果必须使用那么任何将要通过v-html渲染的数据无论来自用户输入、第三方接口还是数据库都必须经过严格的净化处理。永远不要信任任何外部数据。最小化原则使用净化库如 DOMPurify时配置严格的白名单只允许业务确实需要的标签和属性通过。禁止使用过于宽松的配置。6.2 推荐的安全工具库DOMPurify业界标杆一个仅针对 DOM 的、快速、宽容的 XSS 净化工具。它非常容易集成是处理用户生成的 HTML 内容的首选。npm install dompurifyimport DOMPurify from ‘dompurify’ const cleanHTML DOMPurify.sanitize(dirtyHTML, { ALLOWED_TAGS: [‘p’, ‘b’, ‘i’] })vue-dompurify-html一个 Vue 3 指令将 DOMPurify 和v-html无缝结合使用起来更 Vue 风。template div v-dompurify-html“rawHtml”/div /template6.3 集成到开发流程中代码审查Code Review在 Review 时将v-html、dangerouslyUseHTMLString、innerHTML的出现作为重点检查项必须追问其数据来源和净化措施。ESLint 规则可以配置或自定义 ESLint 规则对使用这些危险 API 的代码发出警告或错误并提示使用安全替代方案。// .eslintrc.js 示例使用 eslint-plugin-vue module.exports { rules: { ‘vue/no-v-html’: ‘warn’, // 对使用 v-html 发出警告 } }安全扫描将前端代码安全扫描如使用npm audit、snyk或OWASP ZAP等工具集成到 CI/CD 流水线中定期检查依赖库的已知漏洞。6.4 针对 Element Plus 组件的安全检查清单对于使用 Element Plus 的项目建议定期检查以下组件和属性的使用情况组件风险属性安全建议ElNotificationmessage(当dangerouslyUseHTMLString: true)1. 避免使用。2. 必须用时确保message内容经过净化。ElMessagemessage(当dangerouslyUseHTMLString: true)同上ElMessageBoxmessage(当dangerouslyUseHTMLString: true)同上ElAlertdescription(当支持 HTML 时)查看对应版本文档确认其是否支持 HTML 以及是否有风险开关同上处理。ElTable列formatter函数中返回 HTML避免在formatter中直接返回 HTML 字符串使用渲染函数或作用域插槽。这次对 Element Plus Notification 组件漏洞的深度解析从一个安全提醒开始经历了原理探究、漏洞复现、源码分析、修复实施最后落脚到团队的安全规范建设。它再次印证了一个朴素的道理前端安全尤其是涉及 DOM 操作和渲染的部分容不得半点侥幸。最安全的代码往往来自于对“默认不安全”的深刻认知以及对每一行可能处理外部数据的代码保持审慎的态度。将安全工具和流程嵌入开发的每一个环节远比事后补救更为重要。