概述在现代Web应用开发中富文本编辑器是常见的功能组件但也是XSS跨站脚本攻击的主要入口之一。本文详细介绍如何在Vue项目中使用DOMPurify库来防范富文本编辑器的XSS安全风险。XSS攻击风险分析常见攻击方式脚本注入用户输入scriptalert(XSS)/script事件属性注入用户输入img srcx onerroralert(XSS)链接注入用户输入a hrefjavascript:alert(XSS)点击/a潜在危害窃取用户Cookie和登录凭证劫持用户会话修改页面内容误导用户传播恶意软件DOMPurify简介DOMPurify是一个现代化、快速、兼容性好的DOM净化库专门用于防范XSS攻击。它基于白名单机制只允许安全的HTML标签和属性通过。主要特性零依赖快速高效高度可配置支持多种安全选项安装和基本使用1. 安装DOMPurify1. 安装DOMPurifynpm install dompurify2. 基本用法import DOMPurify from dompurify; const dirty pSome HTML with scriptalert(XSS)/script/p; const clean DOMPurify.sanitize(dirty); console.log(clean); // pSome HTML with /pVue项目中的集成实现1. 创建安全过滤工具函数// utils/sanitize.js import DOMPurify from dompurify; const purifyConfig { // 允许的安全标签 ALLOWED_TAGS: [ p, br, hr, h1, h2, h3, h4, h5, h6, strong, em, u, s, sub, sup, span, a, img, ul, ol, li, blockquote, code, pre, table, thead, tbody, tr, th, td, div, section, article, header, footer, nav, aside ], // 允许的安全属性 ALLOWED_ATTR: [ href, src, alt, title, width, height, style, class, id, data-id, data-type, data-src, data-href ], // 明确禁止的危险标签 FORBID_TAGS: [script, object, embed, iframe, frame, frameset, applet, base, link, meta, style], // 明确禁止的危险属性 FORBID_ATTR: [ onerror, onload, onmouseover, onclick, onfocus, onblur, onsubmit, onchange, javascript:, data:text/html, v-on:, , x-on:, ng-click, ng-bind-html ], USE_PROFILES: { html: true }, SANITIZE_DOM: true }; export const sanitizeHTML (html) { if (!html || typeof html ! string) return ; try { // 预处理移除明显的脚本标签 let cleaned html.replace(/script[^]*.*?\/script/gi, ) .replace(/script[^]*\//gi, ) .replace(/javascript:[^]*/gi, ); // 使用DOMPurify进行深度净化 return DOMPurify.sanitize(cleaned, purifyConfig); } catch (error) { console.warn(DOMPurify sanitization failed:, error); return html.replace(/script[^]*.*?\/script/gi, ).replace(/javascript:[^]*/gi, ); } };2. 富文本编辑器组件集成以下是一个完整的富文本编辑器组件集成了DOMPurify安全过滤!-- components/editor.vue -- template div script :idrandomId namecontent typetext/plain /script /div /template script import _ from lodash; import DOMPurify from dompurify; // 安全配置 const purifyConfig { ALLOWED_TAGS: [ p, br, hr, h1, h2, h3, h4, h5, h6, strong, em, u, s, sub, sup, span, a, img, ul, ol, li, blockquote, code, pre, table, thead, tbody, tr, th, td, div, section, article, header, footer, nav, aside ], ALLOWED_ATTR: [ href, src, alt, title, width, height, style, class, id, data-id, data-type, data-src, data-href ], FORBID_TAGS: [script, object, embed, iframe, frame, frameset, applet, base, link, meta, style], FORBID_ATTR: [ onerror, onload, onmouseover, onclick, onfocus, onblur, onsubmit, onchange, javascript:, data:text/html, v-on:, , x-on:, ng-click, ng-bind-html ], USE_PROFILES: { html: true }, SANITIZE_DOM: true }; // 安全过滤函数 const sanitizeHtml (html) { if (!html || typeof html ! string) return ; try { let cleaned html.replace(/script[^]*.*?\/script/gi, ) .replace(/script[^]*\//gi, ) .replace(/javascript:[^]*/gi, ); return DOMPurify.sanitize(cleaned, purifyConfig); } catch (error) { console.warn(DOMPurify sanitization failed:, error); return html.replace(/script[^]*.*?\/script/gi, ).replace(/javascript:[^]*/gi, ); } }; export default { name: VueUEditor, props: { ueditorPath: { type: String, default: static/umeditor/ }, name: { type: String, default: }, ueditorConfig: { type: Object, default: () ({}) } }, data() { return { randomId: editor_1, instance: null, scriptTagStatus: 0, UEConfig: { autoHeightEnabled: false, allowDivTransToP: false, // UEditor内置过滤规则作为第一道防线 filterRules: { script: function() { return ; }, *: function(tag, attrs) { const filteredAttrs {}; for(const attr in attrs) { if(!attr.startsWith(on)) { filteredAttrs[attr] attrs[attr]; } } return { attrs: filteredAttrs }; } } } }; }, created() { if(this.name ! ud1){ this.randomId this.name; } this.UEConfig _.defaultsDeep({}, this.UEConfig, this.ueditorConfig); if (window.UE ! undefined) { this.scriptTagStatus 2; this.initEditor(); } else { this.insertScriptTag(); } }, beforeDestroy() { if (this.instance ! null this.instance.destroy) { this.instance.destroy(); } }, methods: { // 获取安全内容 getSafeContent() { if (!this.instance) return ; const rawContent this.instance.getContent(); return sanitizeHtml(rawContent); }, // 设置安全内容 setSafeContent(content) { if (!this.instance) return; const safeContent sanitizeHtml(content); this.instance.setContent(safeContent); }, insertScriptTag() { let editorScriptTag document.getElementById(editorScriptTag); let configScriptTag document.getElementById(configScriptTag); if (editorScriptTag null) { configScriptTag document.createElement(script); configScriptTag.type text/javascript; configScriptTag.src this.ueditorPath umeditor.config.js; configScriptTag.id configScriptTag; editorScriptTag document.createElement(script); editorScriptTag.type text/javascript; editorScriptTag.src this.ueditorPath umeditor.js; editorScriptTag.id editorScriptTag; let s document.getElementsByTagName(head)[0]; s.appendChild(configScriptTag); s.appendChild(editorScriptTag); } if (configScriptTag.loaded) { this.scriptTagStatus; } else { configScriptTag.addEventListener(load, () { this.scriptTagStatus; configScriptTag.loaded true; this.initEditor(); }); } if (editorScriptTag.loaded) { this.scriptTagStatus; } else { editorScriptTag.addEventListener(load, () { this.scriptTagStatus; editorScriptTag.loaded true; this.initEditor(); }); } this.initEditor(); }, initEditor() { if (this.scriptTagStatus 2 this.instance null) { this.$nextTick(() { this.instance window.UM.getEditor(this.randomId, this.UEConfig); // 直接向实例添加安全方法 this.instance.getSafeContent this.getSafeContent.bind(this); this.instance.setSafeContent this.setSafeContent.bind(this); setTimeout(() { this.$emit(ready, this.instance); }, 500) }); } } } }; /script3. 父组件中使用安全方法!-- 使用富文本编辑器的组件 -- template div vue-editor :nameud1 storeUEstoreUE :ueditor-configueditorConfig/vue-editor button clicksaveContent保存内容/button /div /template script import VueEditor from /components/editor.vue; export default { components: { VueEditor }, data() { return { editorInstance: null, ueditorConfig: { initialFrameWidth: 100%, initialFrameHeight: 560 } } }, methods: { storeUE(name, editor) { this.editorInstance editor; // 保存编辑器实例 }, saveContent() { if (!this.editorInstance) return; // 使用安全方法获取内容 const safeContent this.editorInstance.getSafeContent(); // 发送到服务器 this.sendToServer(safeContent); }, sendToServer(content) { // 发送经过安全过滤的内容到服务器 console.log(Sending safe content:, content); } } } /script安全最佳实践1. 多层防护策略客户端过滤使用DOMPurify进行前端过滤服务端验证在服务器端再次进行安全验证内容安全策略(CSP)配置HTTP头限制脚本执行2. 严格配置原则采用最小权限原则只允许必要的HTML标签和属性定期审查和更新安全配置对不同类型的用户内容使用不同的安全策略总结通过在Vue项目中正确集成DOMPurify库我们可以有效地防范富文本编辑器的XSS攻击风险。关键要点包括合理的配置根据业务需求制定合适的白名单双重过滤客户端和服务端都进行安全过滤持续监控记录和分析安全事件定期更新随着业务发展调整安全策略实施这套方案后富文本编辑器将具备较强的安全防护能力有效保护应用和用户免受XSS攻击的威胁。