概述
在现代Web应用开发中,富文本编辑器是常见的功能组件,但也是XSS(跨站脚本攻击)的主要入口之一。本文详细介绍如何在Vue项目中使用DOMPurify库来防范富文本编辑器的XSS安全风险。
XSS攻击风险分析
常见攻击方式
- 脚本注入:用户输入
<script>alert('XSS')</script> - 事件属性注入:用户输入
<img src="x" onerror="alert('XSS')"> - 链接注入:用户输入
<a href="javascript:alert('XSS')">点击</a>
潜在危害
- 窃取用户Cookie和登录凭证
- 劫持用户会话
- 修改页面内容误导用户
- 传播恶意软件
DOMPurify简介
DOMPurify是一个现代化、快速、兼容性好的DOM净化库,专门用于防范XSS攻击。它基于白名单机制,只允许安全的HTML标签和属性通过。
主要特性
- 零依赖
- 快速高效
- 高度可配置
- 支持多种安全选项
安装和基本使用
1. 安装DOMPurify
1. 安装DOMPurify
npm install dompurify
2. 基本用法
import DOMPurify from 'dompurify';
const dirty = '<p>Some HTML with <script>alert("XSS")</script></p>';
const clean = DOMPurify.sanitize(dirty); console.log(clean); // '<p>Some HTML with </p>'
Vue项目中的集成实现
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 :id="randomId" name="content" type="text/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)
});
}
}
}
};
</script>
3. 父组件中使用安全方法
<!-- 使用富文本编辑器的组件 -->
<template>
<div>
<vue-editor :name="'ud1'" @storeUE="storeUE" :ueditor-config="ueditorConfig"></vue-editor>
<button @click="saveContent">保存内容</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攻击的威胁。

952

被折叠的 条评论
为什么被折叠?



