3、XSS防御实战:输入过滤、输出编码、CSP策略配置与绕过案例
XSS(跨站脚本攻击),说白了就是攻击者把恶意脚本塞进你的页面里。我见过太多团队在这上面栽跟头了。今天咱们就聊聊怎么防,以及那些你以为防住了、其实根本没防住的坑。
3.1 输入过滤:第一道防线,但不是万能药
很多人一上来就说「过滤用户输入」。嗯,这个思路没错,但容易走偏。我早期做项目时也犯过这个错——以为把 <script> 标签过滤掉就万事大吉了。
实际上,输入过滤要分场景:
- 白名单策略:只允许你明确允许的内容。比如用户填手机号,你就只允许数字和短横线。
- 黑名单策略:拦截已知的危险字符。比如
<script>、onerror=、javascript:等。
举个例子,你以为过滤了 <script>,人家用 <img src=x onerror=alert(1)> 照样能打穿。为什么?因为 onerror 事件根本不需要 <script> 标签。
// 不推荐:只过滤 script 标签
function filterXSS(input) {
return input.replace(/<script>/gi, '');
}
// 推荐:使用成熟的库,比如 DOMPurify
// 或者自己写白名单过滤
function safeFilter(input) {
const allowedTags = ['b', 'i', 'u', 'a'];
// 只保留白名单内的标签,其余全部转义
return sanitize(input, allowedTags);
}
3.2 输出编码:真正该下功夫的地方
输入过滤是防君子不防小人。真正靠谱的,是输出编码。你想想看,用户数据最终要渲染到页面上,只要在「输出」这一步把危险字符转义掉,攻击脚本就执行不了。
输出编码要分上下文:
| 上下文 | 编码方式 | 示例 |
|---|---|---|
| HTML 标签内容 | HTML 实体编码 | < → < |
| HTML 属性值 | 属性编码 | " → " |
| JavaScript 字符串 | JS 转义 | ' → \x27 |
| URL 参数 | URL 编码 | 空格 → %20 |
我曾经在项目中遇到过一个问题:后端把用户昵称直接塞进 <script> 标签的变量里。前端以为后端做了编码,后端以为前端会处理。结果呢?攻击者昵称写了个 ';alert(1)//,直接闭合了引号,执行了恶意代码。
// ❌ 错误做法:直接拼接
const userData = '<script>var name = "' + userName + '";</script>';
// ✅ 正确做法:使用 JSON.stringify 或 encodeURIComponent
const safeName = JSON.stringify(userName);
const userData = `<script>var name = ${safeName};</script>`;
3.3 CSP 策略配置:浏览器级别的最后防线
就算你的过滤和编码都出了问题,CSP(内容安全策略)还能兜底。说白了,CSP 就是告诉浏览器:「哪些资源可以加载,哪些脚本可以执行」。
我建议所有新项目都默认开启 CSP。配置方式很简单,在 HTTP 响应头里加一行:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'unsafe-inline';
几个关键指令:
default-src:默认策略,所有资源类型都适用script-src:控制脚本来源style-src:控制样式来源img-src:控制图片来源connect-src:控制 AJAX、WebSocket 等请求
'unsafe-inline' 和 'unsafe-eval'。这两个一开,CSP 的防护效果直接打五折。
我记得有个客户,他们的 CSP 配了 script-src 'self' 'unsafe-inline'。结果攻击者通过评论功能插入了 <script>alert(1)</script>,照样执行。为什么?因为 'unsafe-inline' 允许所有内联脚本执行。
更好的做法是用 nonce 或 hash:
// 服务端生成随机 nonce 值
const nonce = crypto.randomBytes(16).toString('hex');
res.setHeader('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
// 前端只加载带正确 nonce 的脚本
<script nonce="<%= nonce %>">
// 这是安全的脚本
</script>
3.4 绕过案例:你以为防住了?
讲几个我实际遇到过的绕过案例。看完你就知道,攻击者的脑洞有多大。
案例一:Unicode 编码绕过
有些过滤只检查 ASCII 字符,但浏览器会解析 Unicode 编码。比如:
// 攻击者输入
\u003cscript\u003ealert(1)\u003c/script\u003e
// 如果过滤只检查 < 和 >,这个就漏过去了
// 浏览器解析后,\u003c 会变成 <
案例二:事件处理器绕过
过滤了 onerror,还有 onload、onfocus、onmouseover…… 甚至 onpointerenter:
<input onfocus="alert(1)" autofocus>
<details open ontoggle="alert(1)">
案例三:CSS 注入绕过
你以为 CSS 就安全了?看看这个:
<style>
body {
background-image: url('javascript:alert(1)');
}
</style>
老版本 IE 和某些浏览器会执行 javascript: 伪协议。现在虽然少了,但谁知道呢?
<script> 和 onerror。结果攻击者用 <svg onload=alert(1)> 打穿了。从那以后,我所有富文本内容都强制走 DOMPurify。
3.5 实战建议:三层防御体系
说了这么多,总结一下我的实战经验。防御 XSS 不能只靠一层,要三层叠加:
- 输入层:白名单过滤,只保留你明确允许的内容
- 输出层:根据上下文做正确的编码
- 浏览器层:配置严格的 CSP 策略兜底
这三层只要有一层没破,攻击就成功不了。我现在的项目都是这么配的,几年下来没出过 XSS 漏洞。
最后说一句:安全不是一次性的工作。每次上线前,记得用自动化工具扫一遍。OWASP ZAP 或者 Burp Suite 都行,免费的够用了。