Vue中集成DOMPurify的安全实践指南
在Vue应用中集成DOMPurify是防止XSS攻击的关键措施,特别是在需要渲染HTML内容的场景。以下是详细的实现方案:
完整集成方案
1. 安装依赖
npm install dompurify
# 或
yarn add dompurify
2. 创建DOMPurify封装模块
在src/utils/sanitize.js
中:
import DOMPurify from 'dompurify';
// 自定义安全配置
const sanitizeConfig = {
ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title', 'target', 'class'],
FORBID_ATTR: ['style', 'onclick'],
FORBID_TAGS: ['script', 'style', 'iframe'],
ADD_ATTR: ['target'] // 确保target属性被保留
};
// 创建安全的净化函数
export function sanitizeHTML(dirty) {
// 添加安全钩子
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// 为所有链接添加安全属性
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
});
return DOMPurify.sanitize(dirty, sanitizeConfig);
}
// 创建Trusted Types兼容版本
export function createSafeHTML(dirty) {
return {
__html: sanitizeHTML(dirty)
};
}
3. 创建全局指令(推荐)
在src/directives/sanitize.js
中:
import { sanitizeHTML } from '@/utils/sanitize';
export default {
bind(el, binding) {
if (binding.value) {
el.innerHTML = sanitizeHTML(binding.value);
}
},
update(el, binding) {
if (binding.value) {
el.innerHTML = sanitizeHTML(binding.value);
} else {
el.innerHTML = '';
}
}
};
在main.js
中注册指令:
import Vue from 'vue';
import sanitizeDirective from '@/directives/sanitize';
Vue.directive('sanitize', sanitizeDirective);
4. 在组件中的使用方式
基本使用(指令方式):
<template>
<div>
<!-- 安全渲染HTML内容 -->
<div v-sanitize="unsafeHTML"></div>
</div>
</template>
<script>
export default {
data() {
return {
unsafeHTML: '<p>安全内容</p><script>alert("危险!")</script>'
};
}
};
</script>
使用v-html + 计算属性:
<template>
<div>
<!-- 通过v-html渲染净化后的内容 -->
<div v-html="safeHTML"></div>
</div>
</template>
<script>
import { createSafeHTML } from '@/utils/sanitize';
export default {
data() {
return {
userContent: '<a href="#">用户提交内容</a>'
};
},
computed: {
safeHTML() {
// 返回{ __html: '...' }对象
return createSafeHTML(this.userContent);
}
}
};
</script>
5. 富文本编辑器集成示例
<template>
<div class="rich-text-editor">
<div
contenteditable
ref="editor"
@input="updateContent"
class="editor-content"
></div>
<button @click="saveContent">保存内容</button>
<!-- 预览安全内容 -->
<div class="preview" v-sanitize="purifiedContent"></div>
</div>
</template>
<script>
import { sanitizeHTML } from '@/utils/sanitize';
export default {
data() {
return {
rawContent: '',
purifiedContent: ''
};
},
methods: {
updateContent() {
this.rawContent = this.$refs.editor.innerHTML;
},
saveContent() {
// 保存前净化内容
this.purifiedContent = sanitizeHTML(this.rawContent);
// 发送到API
this.$api.saveContent({
content: this.purifiedContent
});
}
}
};
</script>
<style>
.editor-content {
border: 1px solid #ccc;
padding: 1rem;
min-height: 200px;
margin-bottom: 1rem;
}
.preview {
border: 1px solid green;
padding: 1rem;
margin-top: 1rem;
}
</style>
高级安全策略
1. 使用Trusted Types API(现代浏览器)
在src/utils/sanitize.js
中添加:
export function enableTrustedTypes() {
if (window.trustedTypes && trustedTypes.createPolicy) {
trustedTypes.createPolicy('dompurify', {
createHTML: (input) => DOMPurify.sanitize(input, sanitizeConfig)
});
}
}
在main.js
中调用:
import { enableTrustedTypes } from '@/utils/sanitize';
if (process.client) {
enableTrustedTypes();
}
2. 服务器端渲染(SSR)支持
在Nuxt.js项目中,使用插件方式集成:
plugins/dompurify.client.js
:
import Vue from 'vue';
import DOMPurify from 'dompurify';
// 只在客户端初始化
if (process.client) {
Vue.prototype.$sanitize = (dirty) => {
return DOMPurify.sanitize(dirty, sanitizeConfig);
};
}
在nuxt.config.js
中配置:
export default {
plugins: [
{ src: '~/plugins/dompurify.client.js', mode: 'client' }
]
}
3. 安全配置最佳实践
// 强化安全配置
const strictConfig = {
ALLOWED_TAGS: ['p', 'span', 'br'],
ALLOWED_ATTR: ['class'],
FORBID_ATTR: ['style', 'class', 'id', 'on*'],
ALLOW_DATA_ATTR: false,
RETURN_DOM: false,
RETURN_DOM_FRAGMENT: false,
RETURN_DOM_IMPORT: false,
FORBID_CONTENTS: ['script', 'style', 'svg'],
WHOLE_DOCUMENT: false,
SANITIZE_DOM: true,
KEEP_CONTENT: false,
IN_PLACE: false
};
// 针对特定上下文的宽松配置
const looseConfig = {
ALLOWED_TAGS: [
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'strong', 'em', 'blockquote', 'ul', 'ol',
'li', 'a', 'img', 'br', 'hr', 'div', 'span'
],
ALLOWED_ATTR: ['href', 'src', 'title', 'alt', 'class', 'target'],
FORBID_ATTR: ['style', 'on*'],
ALLOW_DATA_ATTR: false,
ALLOW_UNKNOWN_PROTOCOLS: false,
ADD_URI_SAFE_ATTR: ['href', 'src']
};
4. 自定义钩子增强安全性
添加额外的安全检查:
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// 移除所有事件监听器
Object.keys(node).forEach((key) => {
if (/^on/.test(key)) {
node[key] = null;
}
});
// 处理URL属性
const urlAttrs = ['href', 'src', 'action', 'background'];
urlAttrs.forEach((attr) => {
if (node.hasAttribute(attr)) {
const value = node.getAttribute(attr);
if (/javascript:/i.test(value)) {
node.removeAttribute(attr);
}
}
});
// 移除可疑的协议
if (node.tagName === 'A' && node.href.toLowerCase().startsWith('javascript:')) {
node.removeAttribute('href');
}
});
安全注意事项
-
上下文感知:
- HTML内容 → 使用DOMPurify净化
- URL值 → 使用
URL
构造函数验证
function sanitizeURL(url) { try { const u = new URL(url, window.location.origin); if (['http:', 'https:'].includes(u.protocol)) { return u.toString(); } } catch {} return ''; }
-
内容来源验证:
function isTrustedSource(content) { const hash = sha256(content); return trustedHashes.includes(hash); }
-
性能优化:
import memoize from 'lodash/memoize'; // 缓存净化结果 export const safeHTML = memoize(createSafeHTML);
-
安全日志记录:
DOMPurify.addHook('uponSanitizeElement', (node, data) => { if (data.tagName === 'script') { logSecurityEvent('SCRIPT_REMOVED', node.outerHTML); } });
完整的安全组件示例
components/SafeContent.vue
:
<template>
<div class="safe-content-container">
<div
ref="contentContainer"
:class="containerClass"
v-bind="$attrs"
></div>
</div>
</template>
<script>
import { sanitizeHTML } from '@/utils/sanitize';
import { debounce } from 'lodash';
export default {
name: 'SafeContent',
props: {
content: {
type: String,
default: ''
},
config: {
type: Object,
default: () => ({})
},
containerClass: {
type: String,
default: ''
}
},
watch: {
content: {
immediate: true,
handler: 'updateContent'
}
},
mounted() {
this.updateContent();
this.setupMutationObserver();
},
beforeDestroy() {
if (this.observer) {
this.observer.disconnect();
}
},
methods: {
updateContent: debounce(function() {
if (this.$refs.contentContainer) {
this.$refs.contentContainer.innerHTML = sanitizeHTML(
this.content,
this.config
);
}
}, 300),
setupMutationObserver() {
// 防止运行时修改DOM
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
this.handlePotentialThreat();
}
});
});
this.observer.observe(this.$refs.contentContainer, {
childList: true,
subtree: true
});
},
handlePotentialThreat() {
// 检测到DOM修改时重置内容
console.warn('检测到DOM修改尝试,内容已重置');
this.$refs.contentContainer.innerHTML = sanitizeHTML(this.content);
}
}
};
</script>
总结:最佳实践清单
- 优先使用指令方式:
v-sanitize
指令是最安全的集成方式 - 避免直接使用v-html:除非配合DOMPurify计算属性
- 上下文感知净化:HTML、URL、CSS使用不同的净化策略
- 配置严格白名单:默认使用最严格的配置,根据需求放宽
- 添加安全钩子:强化链接安全性和事件处理
- 监控DOM变化:防止运行时注入
- 服务器端双重验证:即使前端净化,后端仍需验证
- 结合CSP策略:提供第二道防线
- 定期审计配置:随着应用发展调整安全策略
- 安全日志记录:追踪潜在的XSS攻击尝试
通过以上实践,您可以在Vue应用中安全地处理用户生成的HTML内容,有效防御XSS攻击,同时保持应用的灵活性和用户体验。