本文目录导读:

前端防御 XSS(跨站脚本攻击)的核心原则是:不要信任任何用户输入,对所有输出进行编码。
XSS 主要分为三种类型:反射型、存储型、DOM 型,前端编码防御主要针对的是将不可信数据插入到 HTML 上下文时的处理。
以下是前端编码防 XSS 的实战指南和最佳实践:
核心原则:上下文敏感编码(Context-Aware Encoding)
同样是用户数据,插入到 HTML标签体、HTML属性、JavaScript代码 或 URL 中,需要的编码方式完全不同,绝对不能使用单一的转义函数。
| 上下文 (Context) | 编码规则 | 示例 | 风险点 |
|---|---|---|---|
| HTML元素内容 | HTML 实体编码 | 将 < 转为 <,> 转为 > |
插入到 <div>用户数据</div> 中 |
| HTML属性 | 属性值编码 | 将 转为 ", 转为 ' |
插入到 <input value="用户数据"> 中 |
| JavaScript | Unicode 转义 | 将 转为 \x27, 转为 \x22 |
插入到 <script>let x = '用户数据';</script> |
| CSS | CSS 转义 | 非常复杂,强烈建议避免 | 插入到 style="background: url(用户数据)" |
| URL | URL 百分号编码 | 将空格转为 %20, 转为 %22 |
插入到 <a href="用户数据"> |
具体防御措施(按优先级排序)
A. 使用成熟的框架(最推荐)
现代前端框架(如 React、Vue、Angular)在默认情况下会自动进行 HTML 编码,这是最强大的防线。
- React: 在 JSX 中,默认使用
{变量}渲染时,React 会自动对内容进行 HTML 实体转义。- 正确:
<div>{userInput}</div>✅ (安全) - 危险:
<div dangerouslySetInnerHTML={{__html: userInput}} />❌ (必须确认内容是可信的)
- 正确:
- Vue: 使用 双花括号插值时会转义。
- 正确:
<div>{{ userInput }}</div>✅ - 危险:
<div v-html="userInput"></div>❌
- 正确:
- Angular: 使用 或
[property]绑定时会转义。- 危险:
<div [innerHTML]="userInput"></div>❌ (需使用DomSanitizer后且非常谨慎)
- 危险:
只要不使用 innerHTML、outerHTML、v-html、dangerouslySetInnerHTML 等方法,框架本身能解决 90% 的 XSS 问题。
B. 纯前端手动编码(无框架时)
如果必须手动处理,不要自己写转义函数,使用标准的库。
-
浏览器内置 API(推荐):
textContent代替innerHTML: 设置文本内容时,使用element.textContent = userInput;,浏览器会自动处理编码。setAttribute代替字符串拼接属性: 使用element.setAttribute('href', userInput);,但对于 URL 属性(如href、src),仅靠这个不够,需要额外校验协议。createElement和appendChild: 创建 DOM 节点并追加,而不是直接设置 HTML 字符串。
-
成熟的编码库:
- 使用专门的库,如
DOMPurify(用于清理 HTML)、he(HTML 实体编码)。 - 示例 (DOMPurify):
import DOMPurify from 'dompurify'; let clean = DOMPurify.sanitize(dirtyInput); document.getElementById('app').innerHTML = clean; // 仅在需要渲染富文本时使用
- 使用专门的库,如
C. 严格的 CSP(内容安全策略)—— 纵深防御
即使代码在编码上出了 Bug,CSP 可以作为最后一道防线,通过 HTTP 响应头配置。
- 关键指令:
script-src 'self': 只允许加载同源的脚本,禁止内联脚本(<script>alert(1)</script>)和eval()。object-src 'none': 禁止<object>、<embed>、<applet>
- 示例:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
D. 特殊场景处理
-
URL 插入 (
<a href="">,<img src="">):- 必须进行协议验证,防止执行
javascript:alert(1)攻击。 - 正确做法:
function sanitizeUrl(url) { // 只允许 http, https, mailto 等安全协议 const allowed = /^(https?|ftp|mailto|tel|#):/i; if (allowed.test(url)) { return url; } // 如果不匹配,返回空或安全的默认值 return 'about:blank'; }
- 必须进行协议验证,防止执行
-
富文本编辑(允许用户输入 HTML):
- 绝对不要直接拼接字符串。
- 必须使用经过严格测试的库进行白名单过滤。
- 推荐:
DOMPurify(最流行,GitHub 17k+ stars)或sanitize-html(Node.js 端)。 - 不推荐:自己写正则过滤黑名单(如过滤
<script>),攻击者很容易绕过(如使用<img src=x onerror=alert(1)>)。
-
JSON 嵌入到
<script>:- 场景:服务端将 JSON 数据直接写在 HTML 页面中(如 SSR)。
- 风险:JSON 字符串中包含
</script>,会提前闭合标签造成 XSS。 - 防御:使用正规的 JSON 序列化库(如
JSON.stringify),并在字符串中正确转义<和>。
-
React dangerouslySetInnerHTML / Vue v-html / Angular [innerHTML]:
- 核心铁律:传递给这些属性的内容,必须先用
DOMPurify清洗。 -
// React 示例 import DOMPurify from 'dompurify'; const sanitizedHTML = DOMPurify.sanitize(userProvidedRichText); return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
- 核心铁律:传递给这些属性的内容,必须先用
常见误区与避坑
- 误区:在服务端用 HttpOnly Cookie 保护了 Token,前端就不用防 XSS 了。
- 真相:XSS 不仅能偷 Cookie,还能窃取页面内容、伪造请求、键盘记录,即使 HttpOnly 防住了 Cookie,攻击者依然可以执行转账操作。
- 误区:使用
encodeURI或encodeURIComponent就能防住所有 XSS。- 真相:它们只对 URL 中的特殊字符进行百分号编码,如果直接把编码后的字符串插入到
<div>标签体中,<和>仍然是危险的。
- 真相:它们只对 URL 中的特殊字符进行百分号编码,如果直接把编码后的字符串插入到
- 误区:过滤掉
<script>标签就安全了。- 真相:XSS 向量远不止
<script>,还有事件处理器(<img src=x onerror=alert(1)>)、<iframe>、<link>、<svg>等,黑名单过滤永远有漏洞。
- 真相:XSS 向量远不止
- 避免使用
eval()和new Function()。- 用户输入的数据永远不要通过
eval()执行。
- 用户输入的数据永远不要通过
前端编码防 XSS 清单
- 默认防御:使用 React/Vue/Angular 框架,避免使用
innerHTML等危险 API。 - 手动编码:无框架时,使用
textContent、setAttribute或调用DOMPurify库。 - URL 处理:插入
href/src前,校验协议只能为https?或mailto。 - 富文本:必用白名单清洗库(
DOMPurify)。 - 纵深防御:配置严格的 CSP 响应头,禁止内联脚本。
- 避免风险:禁止
eval(),谨慎使用innerHTML。