直接用innerhtml渲染用户输入等于开后门,必须按上下文转义;textcontent可安全显示纯文本,但富文本需dompurify白名单过滤;csp不能替代前端转义,xss防护需覆盖输入、存储、输出、渲染全链路。

直接用 innerHTML 渲染用户输入,等于把 DOM 的钥匙交到攻击者手上——几乎所有存储型和反射型 XSS 漏洞,都始于这一行代码。
哪些地方最容易漏掉转义?
不是所有“插内容”的操作都危险,但以下三类场景几乎必踩坑:
- 用
element.innerHTML = userInput替换评论、消息、富文本预览区域 - 拼接 URL 时直接把用户输入塞进
location.href或a.href,比如a.href = '/user/' + id(id来自 URL 参数且未校验) - 在
<script></script>标签内动态注入 JSON 数据,写成<script>window.data = ${JSON.stringify(userInput)}</script>(未对字符串做 JS 上下文编码)
这些位置的共同点是:数据未经上下文感知处理,就进入了 HTML 解析器或 JS 引擎。浏览器不会“猜”你本意是显示还是执行——它只认语法。
textContent 能替代 innerHTML 吗?
能,但只适用于纯文本场景。它的作用是把内容当字符串插入,自动转义所有 HTML 特殊字符。
比如:
const el = document.getElementById('comment');
el.textContent = '<script>alert(1)</script>'; // 页面显示的就是这串文字,不执行
但如果你需要保留用户输入中的 <b></b>、<code> 等有限格式,textContent 就失效了。这时候必须走白名单过滤,而不是退回到 innerHTML。
容易被忽略的细节:textContent 不会解析实体编码(如 <),它原样输出。所以如果后端返回的是已 HTML 编码的字符串,前端再用 textContent 插入,用户看到的就是 <script>,而非 <script></script> —— 这属于前后端编码重复,需统一约定:后端只负责存储原始内容,转义由前端按上下文决定。
为什么正则过滤 HTML 标签是高危操作?
因为 HTML 是上下文敏感语言,正则无法可靠解析嵌套、注释、属性引号、CDATA 区块等结构。一个典型失败案例:
攻击者输入:
@@##@@
你以为删掉 onerror 就安全了?试试这个:
@@##@@<!-- @@##@@ -->
或者更隐蔽的:
<svg><script>alert(1)</script></svg>
正则很难覆盖所有变体。真正可靠的方案只有两个:DOMPurify.sanitize()(基于真实 DOM 解析)或服务端白名单过滤(如 markdown-wasm 配置只允许 <b></b>、<i></i> 等少数标签)。
CSP 头不能代替前端转义
Content-Security-Policy 是防御层,不是兜底层。它能阻止 eval()、内联脚本、非白名单域名的 script 加载,但它拦不住以下情况:
- 攻击者通过
javascript:alert(1)伪协议触发 XSS(script-src对此无效) - 恶意代码已存在于页面中,比如被污染的第三方库或缓存污染
- 攻击利用的是
document.write()或location.replace()等 DOM 操作,绕过 CSP 的 script 执行限制
换句话说,CSP 是“锁门”,而转义是“不给攻击者进门的机会”。门锁再牢,也不能让陌生人站在你家客厅里。
最常被忽略的一点:XSS 防御不是单点任务,而是贯穿输入、存储、输出、渲染四个环节的链路。任何一个环节松动,整条防线就失效。尤其要注意服务端返回的数据是否已做过一次转义——前端再做一遍,可能造成双重编码;不做,又留了缺口。必须明确每个环节的职责边界。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











