从前端开发看懂 DOM 型 XSS:原理 + 防御(用 Vue/React 举例子)
从前端开发看懂 DOM 型 XSS:原理 + 防御(用 Vue/React 举例子)

“做了 2 年前端开发,每天用 Vue 的v-html渲染富文本、用 React 的dangerouslySetInnerHTML展示后端内容,却不知道这些写法可能藏着 DOM 型 XSS 漏洞;听说 XSS 能偷用户 Cookie,却分不清‘反射型’和‘DOM 型’的区别”—— 这是前端转行安全时最常见的认知盲区。
其实 DOM 型 XSS 是前端开发者 “最容易接触到的漏洞”,它不依赖服务器输出,完全由客户端 DOM 解析触发,你的innerHTML、location.hash、document.write等日常操作,都可能成为攻击入口。本文用你熟悉的 Vue/React 代码举例,从 “原理→风险场景→防御” 全程衔接前端技能,让你看完就能改造项目中的不安全代码。
一、先搞懂:DOM 型 XSS 的 “3 个核心认知”(和前端开发强相关)
不用背学术定义,从前端视角理解 DOM 型 XSS 的本质:
1. 本质:“客户端 DOM 解析导致的脚本执行”
DOM 型 XSS 和反射型 / 存储型的核心区别:
-
反射型 / 存储型:恶意脚本由服务器输出到页面(比如后端把用户输入的
前端视角举例:
后端返回的 URL 参数是?content=用户输入,前端用document.querySelector(‘#box’).innerHTML = location.hash.slice(1)将参数插入页面 —— 若用户输入,前端直接解析执行,这就是 DOM 型 XSS。
2. 关键条件:2 个 “前端操作” 缺一不可
DOM 型 XSS 必须满足两个条件,都是前端开发者能直接控制的:
-
可控输入:输入来源可控(如 URL 参数、localStorage、用户输入框内容);
-
危险 DOM 操作:用不安全的 API 将输入插入 DOM(如innerHTML、document.write、eval)。
前端开发常见的 “危险操作” 清单:
| 危险 API / 写法 | 安全替代方案 | 前端场景举例 |
|---|---|---|
| element.innerHTML = 输入 | element.textContent = 输入 | 渲染用户评论、富文本内容 |
| document.write(输入) | document.createTextNode(输入) | 动态添加页面内容 |
| eval(输入) | 用JSON.parse替代(若解析 JSON) | 解析 URL 参数中的 JSON 字符串 |
| setTimeout(输入, 1000) | 传入函数而非字符串 | 动态执行延迟代码 |
3. 危害:和前端开发的 “用户数据安全” 直接相关
DOM 型 XSS 能直接窃取前端可控的敏感数据:
-
偷 Cookie:document.cookie获取用户登录凭证,发送到黑客服务器;
-
篡改页面:把 “登录按钮” 改成 “转账按钮”,诱导用户操作;
-
劫持操作:自动模拟用户点击(如关注黑客账号、提交表单)。
二、Vue/React 中的 “DOM 型 XSS 风险场景”(附代码对比)
前端框架虽做了部分防御(如 Vue 的{{}}自动转义),但开发者的 “不当用法” 仍会触发漏洞,以下是 Vue/React 中最常见的 3 个风险场景,每个场景都附 “不安全代码 vs 安全代码” 对比。
1. 场景 1:Vue 中的v-html滥用(最易踩坑)
风险原理:
Vue 的v-html会将内容解析为 HTML,而非纯文本 —— 若内容含恶意脚本,会直接执行({{}}插值会自动转义,无此风险)。
代码对比:
<!-- ❶ 不安全代码:直接用v-html渲染用户可控内容 -->
<template>
<div class="comment-list">
<!-- 风险:用户输入的comment若为`<script>alert(document.cookie)</script>`,会执行脚本 -->
<div v-for="comment in comments" :key="comment.id" v-html="comment.content"></div>
</div>
</template>
<script>
export default {
data() {
return {
comments: [] // 从API获取的用户评论,content字段可控
};
},
mounted() {
// 模拟从API获取评论(含恶意内容)
this.comments = [
{ id: 1, content: "<p>正常评论</p>" },
{ id: 2, content: "<script>alert('偷Cookie')</script>" } // 恶意评论
];
}
};
</script>
<!-- ❷ 安全代码:方案1(优先):用v-text渲染纯文本,不解析HTML -->
<template>
<div class="comment-list">
<div v-for="comment in comments" :key="comment.id" v-text="comment.content"></div>
</div>
</template>
<!-- ❷ 安全代码:方案2(需渲染HTML时):先过滤危险标签/属性 -->
<template>
<div class="comment-list">
<div v-for="comment in safeComments" :key="comment.id" v-html="comment.safeContent"></div>
</div>
</template>
<script>
// 引入HTML过滤库(推荐DOMPurify,轻量且全面)
import DOMPurify from 'dompurify';
export default {
data() {
return {
comments: [],
safeComments: []
};
},
mounted() {
this.comments = [/* 从API获取评论 */];
// 过滤危险内容:保留<p><img>等安全标签,删除<script><svg>及onclick/onerror等事件
this.safeComments = this.comments.map(comment => ({
...comment,
safeContent: DOMPurify.sanitize(comment.content)
}));
}
};
</script>
前端转行优势衔接:
你平时用v-html渲染富文本的场景(如博客正文、商品描述),正是 DOM 型 XSS 的高发区。现在只需加一步 “DOMPurify 过滤”,就能复用原有渲染逻辑,同时实现安全防御。
2. 场景 2:React 中的dangerouslySetInnerHTML(命名已提示风险)
风险原理:
React 默认禁止直接插入 HTML,dangerouslySetInnerHTML是 “强制开启 HTML 解析” 的特殊 API—— 名字中的 “dangerously” 已提示风险,若内容可控且未过滤,必触发 DOM 型 XSS。
代码对比:
// ❶ 不安全代码:直接用dangerouslySetInnerHTML渲染用户输入
import React, { useState, useEffect } from 'react';
function CommentList() {
const [comments, setComments] = useState([]);
useEffect(() => {
// 模拟从API获取含恶意内容的评论
setComments([
{ id: 1, content: "<p>正常评论</p>" },
{ id: 2, content: "<img src=x onerror=alert(document.cookie)>"} // 恶意内容(图片加载失败触发脚本)
]);
}, []);
return (
<div className="comment-list">
{comments.map(comment => (
<div
key={comment.id}
// 风险:恶意content中的onerror事件会执行
dangerouslySetInnerHTML={{ __html: comment.content }}
/>
))}
</div>
);
}
// ❷ 安全代码:用DOMPurify过滤后再渲染
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify'; // 引入过滤库
function CommentList() {
const [comments, setComments] = useState([]);
const [safeComments, setSafeComments] = useState([]);
useEffect(() => {
const rawComments = [/* 从API获取评论 */];
// 过滤危险内容:删除<script>、onerror等风险项
const filtered = rawComments.map(comment => ({
...comment,
safeContent: DOMPurify.sanitize(comment.content)
}));
setSafeComments(filtered);
}, []);
return (
<div className="comment-list">
{safeComments.map(comment => (
<div
key={comment.id}
dangerouslySetInnerHTML={{ __html: comment.safeContent }}
/>
))}
</div>
);
}
避坑提示:
React 的{{}}插值会自动转义(如
3. 场景 3:URL 参数解析导致的 DOM 型 XSS(前端路由常见)
风险原理:
前端通过location.hash、URLSearchParams获取 URL 参数后,若直接用innerHTML插入 DOM,攻击者可构造含恶意脚本的 URL(如http://xxx.com/#%3Cscript%3Ealert(1)%3C/script%3E),诱导用户点击触发漏洞。
Vue Router 示例(代码对比):
<!-- ❶ 不安全代码:直接解析hash参数插入DOM -->
<template>
<div class="user-profile">
<h2>用户信息</h2>
<div id="profile-content"></div> <!-- 用原生DOM操作插入内容 -->
</div>
</template>
<script>
export default {
mounted() {
// 从hash获取用户输入的内容(如URL为#<script>alert('XSS')</script>)
const userContent = window.location.hash.slice(1); // 截取#后的内容
// 风险:直接用innerHTML插入,恶意脚本会执行
document.getElementById('profile-content').innerHTML = userContent;
}
};
</script>
<!-- ❷ 安全代码:用textContent替代innerHTML,或过滤后插入 -->
<template>
<div class="user-profile">
<h2>用户信息</h2>
<div id="profile-content"></div>
</div>
</template>
<script>
import DOMPurify from 'dompurify';
export default {
mounted() {
const userContent = window.location.hash.slice(1);
// 方案1:纯文本展示(优先)
document.getElementById('profile-content').textContent = userContent;
// 方案2:需渲染HTML时,先过滤
// const safeContent = DOMPurify.sanitize(userContent);
// document.getElementById('profile-content').innerHTML = safeContent;
}
};
</script>
前端转行优势衔接:
你平时用 Vue Router、React Router 处理 URL 参数的场景(如详情页/detail?id=123),若参数值需展示在页面,一定要警惕 —— 哪怕是id这类看似 “无风险” 的参数,若攻击者构造id=,且前端用innerHTML渲染,仍会触发漏洞。
三、前端可直接落地的 “DOM 型 XSS 防御方案”(附复用代码)
防御 DOM 型 XSS 不需要复杂的后端改造,前端开发者用现有技能就能实现,核心是 “禁用危险 API + 过滤可控输入”,以下是 4 个可直接复用的方案。
1. 方案 1:用安全的 DOM API 替代危险 API(最基础)
这是防御的 “第一道防线”,优先用不解析 HTML 的 API,从源头避免脚本执行:
| 前端场景 | 危险写法 | 安全写法 |
|---|---|---|
| 渲染用户输入 | el.innerHTML = userInput | el.textContent = userInput |
| 动态添加文本内容 | document.write(text) | const textNode = document.createTextNode(text); el.appendChild(textNode) |
| 解析 URL 参数并展示 | el.innerHTML = location.search | el.textContent = location.search |
| 动态设置链接 | a.href = ‘javascript:’ + input | 禁止javascript:伪协议,用正常 URL |
复用代码:封装安全的 DOM 渲染函数
// 安全渲染工具函数:根据需求选择“纯文本”或“过滤后HTML”
export const safeRender = {
// 纯文本渲染(推荐优先使用)
text(el, content) {
if (el && el.textContent !== undefined) {
el.textContent = content || '';
}
},
// 过滤后HTML渲染(需渲染富文本时用)
html(el, content) {
if (!el) return;
// 引入DOMPurify(npm install dompurify)
const DOMPurify = require('dompurify');
const safeContent = DOMPurify.sanitize(content || '');
el.innerHTML = safeContent;
}
};
// 使用示例:
// safeRender.text(document.getElementById('box'), userInput); // 纯文本
// safeRender.html(document.getElementById('box'), richText); // 过滤后HTML
2. 方案 2:用 DOMPurify 过滤危险 HTML(富文本场景必备)
当必须渲染 HTML(如博客正文、富文本编辑器内容)时,手动过滤标签 / 属性容易遗漏风险,推荐用成熟库DOMPurify(轻量,压缩后仅 20KB,支持所有现代浏览器)。
基础使用步骤:
-
安装:npm install dompurify --save;
-
引入并使用:
import DOMPurify from 'dompurify';
// 1. 过滤恶意HTML(保留安全标签如<p><img>,删除<script><svg>)
const rawHTML = '<script>alert(1)</script><p>安全内容</p><img src=x onerror=alert(2)>';
const safeHTML = DOMPurify.sanitize(rawHTML);
// safeHTML结果:'<p>安全内容</p><img src="x">'(onerror事件被删除)
// 2. 自定义过滤规则(如只允许<p><a>标签)
const customSafeHTML = DOMPurify.sanitize(rawHTML, {
ADD_TAGS: [], // 额外允许的标签
ALLOWED_TAGS: ['p', 'a'], // 仅允许的标签
ALLOWED_ATTR: ['href', 'title'] // 仅允许的属性(a标签只允许href和title)
});
框架集成:Vue 全局指令封装
// 1. 在main.js中注册全局v-safe-html指令
import Vue from 'vue';
import DOMPurify from 'dompurify';
Vue.directive('safe-html', {
bind(el, binding) {
// 过滤后渲染HTML
el.innerHTML = DOMPurify.sanitize(binding.value);
},
update(el, binding) {
// 数据更新时重新过滤
el.innerHTML = DOMPurify.sanitize(binding.value);
}
});
// 2. 组件中使用(替代v-html)
<template>
<div v-safe-html="richText"></div>
</template>
3. 方案 3:配置 CSP(内容安全策略)—— 兜底防御
CSP 是浏览器提供的 “强制安全策略”,即使前端代码有疏漏,CSP 也能阻止恶意脚本执行,相当于 “第二道防线”。前端开发者可通过meta标签或后端响应头配置。
前端meta标签配置(快速生效):
<!-- 放在<head>标签中,限制脚本加载和执行来源 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self'; // 默认只允许加载自身域名资源
script-src 'self'; // 只允许执行自身域名的脚本(禁止inline script和eval)
style-src 'self'; // 只允许加载自身域名的样式
img-src 'self' data:; // 图片允许自身域名和data:协议
object-src 'none'; // 禁止加载插件(如Flash)
base-uri 'self'; // 限制base标签的href
">
关键作用:
-
禁止inline script(如)执行;
-
禁止eval、setTimeout(“字符串”)等动态执行脚本;
-
只允许加载信任域名的资源(避免加载黑客服务器的恶意脚本)。
4. 方案 4:过滤危险输入(前端 + 后端双重验证)
前端在接收用户输入(如评论、表单)时,提前过滤危险字符 / 事件,后端同步做相同过滤(避免前端过滤被绕过,如攻击者禁用 JS)。
前端过滤函数(复用代码):
/**
* 过滤用户输入中的危险内容
* @param {string} input - 用户输入内容
* @returns {string} 过滤后的安全内容
*/
export function filterDangerousInput(input) {
if (typeof input !== 'string') return '';
// 1. 过滤<script>、<svg>等危险标签(大小写不敏感)
const tagFilter = input.replace(/<\/?script|svg|iframe|on\w+/gi, match => {
return match.replace(/[a-zA-Z]/g, char => `_${char}_`); // 替换为无害字符
});
// 2. 过滤javascript:伪协议
const protocolFilter = tagFilter.replace(/javascript:/gi, 'javascript_disabled:');
// 3. 过滤危险事件属性(如onclick、onerror)
const eventFilter = protocolFilter.replace(/on\w+=/gi, 'on_event_disabled=');
return eventFilter;
}
// 使用示例:用户提交评论前过滤
const userComment = "<script>alert(1)</script>点击我";
const safeComment = filterDangerousInput(userComment);
// safeComment结果:"<_s_c_r_i_p_t_>alert(1)</_s_c_r_i_p_t_>点击我"
四、前端转行安全的 “DOM 型 XSS 避坑指南”
- 坑 1:以为 “框架自带防御就不用管”
Vue 的{{}}、React 的{{}}虽自动转义,但v-html、dangerouslySetInnerHTML是 “例外”—— 这些 API 需要手动过滤,框架不会自动处理风险。
- 坑 2:只过滤**
攻击者可通过、
- 坑 3:前端过滤后,后端不做二次过滤
攻击者可禁用浏览器 JS 绕过前端过滤(如用 Postman 直接提交恶意内容),后端必须做和前端相同的过滤逻辑,实现 “双重保险”。
- 坑 4:滥用eval解析 JSON 数据
前端解析 URL 参数中的 JSON 时,用eval(jsonStr)会触发 XSS(如jsonStr=“alert(1)”),正确做法是用JSON.parse(jsonStr),即使内容含恶意脚本也会被当作字符串处理。
五、实战任务:改造有 DOM 型 XSS 风险的 Vue 组件
用本文所学,改造以下有风险的 Vue 组件(答案附后):
<!-- 有风险的组件:用v-html渲染URL参数中的内容 -->
<template>
<div class="search-result">
<h3>搜索结果:</h3>
<div v-html="searchQuery"></div>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: ''
};
},
mounted() {
// 从URL参数获取搜索关键词(如?query=<script>alert(document.cookie)</script>)
const params = new URLSearchParams(window.location.search);
this.searchQuery = params.get('query') || '无结果';
}
};
</script>
改造答案(安全版本):
<template>
<div class="search-result">
<h3>搜索结果:</h3>
<!-- 方案1:纯文本渲染(推荐,搜索关键词无需HTML) -->
<div v-text="searchQuery"></div>
<!-- 方案2:若需渲染HTML(如关键词高亮),加DOMPurify过滤 -->
<!-- <div v-safe-html="searchQuery"></div> -->
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: ''
};
},
mounted() {
const params = new URLSearchParams(window.location.search);
const rawQuery = params.get('query') || '无结果';
// 额外加一层前端过滤(后端同步过滤)
this.searchQuery = this.filterDangerousInput(rawQuery);
},
methods: {
filterDangerousInput(input) {
// 过滤危险标签和事件
return input.replace(/<\/?script|on\w+|javascript:/gi, '');
}
}
};
</script>
六、最后:前端开发者的 “DOM 型 XSS 防御竞争力”
很多前端转行群体觉得 “安全是后端的事”,但 DOM 型 XSS 恰恰是前端能直接掌控的漏洞 —— 你的 DOM 操作经验、框架使用技能,都是防御的核心优势:
-
你熟悉innerHTML和textContent的区别,能快速替换危险 API;
-
你懂 Vue/React 的指令和 API,能封装安全的渲染组件(如v-safe-html);
-
你每天处理用户输入和 URL 参数,能提前识别风险场景。
防御 DOM 型 XSS 不需要你成为 “安全专家”,只需在日常开发中多做 3 件事:
-
非必要不用v-html、dangerouslySetInnerHTML;
-
用textContent或DOMPurify处理所有可控输入;
-
给项目加 CSP 配置兜底。
下次写代码时,看到innerHTML或dangerouslySetInnerHTML,不妨多问一句:“这个内容是用户可控的吗?有没有风险?”—— 你的这一个小疑问,可能就避免了一次 XSS 攻击。
网络安全学习资料分享
为了帮助大家更好的学习网络安全,我把我从一线互联网大厂薅来的网络安全教程及资料分享给大家,里面的内容都是适合零基础小白的笔记和资料,不懂编程也能听懂、看懂,朋友们如果有需要这套网络安全教程+进阶学习资源包,可以扫码下方二维码限时免费领取(如遇扫码问题,可以在评论区留言领取哦)~


更多推荐


所有评论(0)