Vue项目中使用DOMPurify防范富文本编辑器XSS攻击的完整指南

概述

在现代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攻击风险。关键要点包括:

  1. 合理的配置:根据业务需求制定合适的白名单
  2. 双重过滤:客户端和服务端都进行安全过滤
  3. 持续监控:记录和分析安全事件
  4. 定期更新:随着业务发展调整安全策略

实施这套方案后,富文本编辑器将具备较强的安全防护能力,有效保护应用和用户免受XSS攻击的威胁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值