本文目录导读:

- 核心原则:输出编码 > 输入过滤
- 输入验证(Input Validation) – 第一道防线
- 输出编码(Output Encoding) – 最核心的防御
- 内容安全策略(Content Security Policy, CSP) – 强安全头部
- 针对特定场景的防御
- 实战过滤工具与库(强烈推荐)
- 一个有效的过滤流程
针对XSS跨站脚本攻击的防御,核心原则是永远不要信任用户的输入,有效的过滤并非单一技术,而是一个多层防御体系,以下是从输入到输出的全链路有效过滤与防御策略。
核心原则:输出编码 > 输入过滤
最可靠的方法是输出编码(Output Encoding),而不是试图阻止恶意输入进入系统。 因为输入过滤很容易被绕过(例如使用不同编码、大小写变化等),而输出编码可以确保任何数据在展示时都只是普通文本,不会被浏览器解析为脚本。
输入验证(Input Validation) – 第一道防线
虽然不能完全依赖,但可以显著减少攻击面。
- 白名单策略(最安全):
- 只允许已知、安全的字符,手机号只允许数字、;用户名只允许字母、数字、下划线。
- 禁止黑名单:不要试图去过滤
<script>、onerror等关键词,攻击者有无数种绕过方式(如<img src=x onerror=alert(1)>可以写成\x3cimg src=x onerror=alert(1)\x3e)。
- 数据格式校验:
- 数据类型:强制为整数、浮点数、布尔值。
- 长度限制:合理限制输入长度,增加攻击成本。
- 正则表达式:对于邮箱、URL、日期等,使用严格的正则匹配格式。
输出编码(Output Encoding) – 最核心的防御
这是应对XSS最有效、最通用的方法,根据数据放置的上下文,使用不同的编码方式。
| 输出位置 | 编码方式 | 示例 (以HTML为例) | 说明 |
|---|---|---|---|
HTML标签内部 (如 <div>用户数据</div>) |
HTML实体编码 | < → <> → >→ "→ '& → & |
这是最常见的场景,确保所有动态文本都使用 textContent 而非 innerHTML。 |
HTML属性内 (如 <input value="用户数据">) |
HTML属性编码 | 除了HTML实体编码,还需对空格、制表符等特殊字符编码。 | 注意,不要用 包裹用户数据时,只需转义 本身。 |
JavaScript字符串内 (如 <script>var a = '用户数据'</script>) |
JavaScript转义 | → → → \n → \\n |
极不推荐 在 <script> 中直接拼接用户数据,极易出错,优先使用 textContent 或 innerText。 |
URL参数内 (如 <a href="?page=用户数据">) |
URL编码(Percent Encoding) | 空格 → %20< → %3C> → %3E→ %22 |
必须先进行URL编码,然后再进行HTML属性编码(如果需要放在HTML属性中)。 |
CSS内 (如 <style> .user { background: url("用户数据"); } </style>) |
CSS转义 | 对所有非字母数字字符使用 加十六进制转义,如 → \27 |
除非绝对必要,永远不要将用户数据直接放入CSS。 |
重要提醒:现代Web框架(如React、Vue、Angular、Spring Boot、Django等)都内置了自动输出编码。只要使用模板引擎的标准语法(如 React 的 , Angular 的 ),就自动受到保护。 只有当你手动使用 innerHTML、v-html(Vue)、dangerouslySetInnerHTML(React)或拼接SQL字符串时,才会出现漏洞。
内容安全策略(Content Security Policy, CSP) – 强安全头部
CSP 是一个 HTTP 响应头,它告诉浏览器只执行来自特定来源的脚本,从而有效阻止内联脚本和执行恶意代码。
-
基本配置:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';default-src 'self':只允许从同源(自己的域名)加载资源。script-src 'self':只允许执行同源JavaScript文件。object-src 'none':禁止<object>、<embed>、<applet>等插件。
-
进阶配置(推荐):
- 禁止内联脚本:
script-src 'self'已经禁止了<script>alert(1)</script>。 - 禁止
eval:script-src 'self' 'unsafe-inline'(默认允许,除非明确禁用) → 应使用'unsafe-eval'仅当需要时。 - 使用 Nonce(一次性随机数):为可信的内联脚本生成一次性随机数。
Content-Security-Policy: script-src 'nonce-{random-value}'HTML 中对应:
<script nonce="{random-value}">alert('安全的内联脚本')</script> - 使用 Hash:对可信内联脚本内容计算哈希值。
Content-Security-Policy: script-src 'sha256-{hash-value}'
- 禁止内联脚本:
-
报告模式:先使用
Content-Security-Policy-Report-Only排查问题,收集违规报告,确认无误后再切换为强制执行模式。
针对特定场景的防御
| 场景 | 防御措施 |
|---|---|
| 富文本编辑器(如 TinyMCE, CKEditor) | 使用专门的 HTML 净化库 (如 DOMPurify, Bleach),这些库会解析HTML,移除所有不安全标签(如 <script>)、事件处理属性(如 onerror)、危险的 href(如 javascript:)等。 |
| JSON 响应 | 确保 Content-Type 为 application/json,浏览器不会将其当作HTML解析。永远不要在 JSON 响应中直接返回用户输入作为 JavaScript 字符串,需进行 JSON 转义。 |
| 服务器端 vs 客户端 | 双重防御:服务器端必须做输出编码(因为客户端可能被禁用或篡改),客户端做辅助检查。 |
| 文件上传 | 将上传的文件存储在 不同的域名 下(如 upload.example.com, 而不是 www.example.com),避免同源策略下的XSS,不要直接显示用户上传的文件名作为 <img> 标签的 src,应将其作为资源ID,通过服务器端处理。 |
实战过滤工具与库(强烈推荐)
不要自己写过滤函数,容易遗漏,使用行业公认的库:
- 前端 JavaScript:
- DOMPurify:用于净化富文本HTML,支持配置白名单标签和属性,几乎所有现代富文本编辑器都依赖它。
- sanitize-html:Node.js端的HTML净化器。
- 后端:
- Java:
OWASP Java HTML Sanitizer或JSoup(用于富文本净化)。 - Python:
Bleach。 - PHP:
HTML Purifier。 - .NET:
HtmlSanitizer库。
- Java:
一个有效的过滤流程
- 源头控制:用户输入时进行白名单校验(如只允许字母数字)。
- 存储:按原样存储(不进行HTML转义存储,因为输出场景不同)。
- 输出(关键):
- 普通文本:使用模板引擎的自动编码(如 )。
- 属性值:使用属性编码。
- URL:使用URL编码,然后嵌入到HTML中。
- 富文本:使用 DOMPurify 或类似库。
- 全局控制:配置 Content-Security-Policy 头部。
- 定期审计:使用自动扫描工具(如 OWASP ZAP, Burp Suite)检测是否存在绕过。
一句话总结:永远不要在HTML、JavaScript、URL或CSS中直接拼接用户输入,使用框架的自动编码 + CSP + 合理的输入校验 = 最安全的防御。