3、XSS防御实战:输入过滤、输出编码、CSP策略配置与绕过案例
聊到XSS防御,我见过太多人一上来就甩一句「过滤掉特殊字符就行了」。嗯,要是真这么简单,安全工程师早该失业了。XSS的防御是个系统工程,不是单靠一个函数就能搞定的。今天咱们就把这块掰开揉碎了讲。
3.1 输入过滤:别把宝全押在这
输入过滤,说白了就是不让恶意数据进到系统里。但这里有个坑——你永远不知道用户的数据会流向哪里。
白名单 vs 黑名单
我个人习惯用白名单。黑名单就像打地鼠,你封了<script>,攻击者就用<img onerror>;你封了onerror,人家又搞出<svg onload>。没完没了。
白名单示例:只允许特定标签和属性
// 用DOMPurify做白名单过滤
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title', 'target'],
ALLOW_DATA_ATTR: false
});
我在项目中遇到过,有人自己写正则过滤<script>,结果攻击者用<ScRiPt>大小写混写就绕过去了。所以啊,别自己造轮子,用成熟的库。
注意:输入过滤不能作为唯一防线。数据可能经过多层流转,比如从API来、从数据库来,这些地方你没法保证都过滤了。
3.2 输出编码:这才是真正的护城河
输出编码,说白了就是把数据里的特殊字符转义掉,让浏览器不把它当代码执行。这是防御XSS最核心的一步。
不同上下文,不同编码方式
你想想看,数据出现在HTML标签里、出现在JavaScript里、出现在URL里,编码方式能一样吗?当然不能。
| 上下文 | 编码方式 | 示例 |
|---|---|---|
| HTML标签内容 | HTML实体编码 | < → < |
| HTML属性值 | HTML属性编码 | " → " |
| JavaScript字符串 | Unicode转义 | ' → \x27 |
| URL参数 | URL编码 | & → %26 |
小技巧:在React、Vue这类框架里,默认的模板语法已经做了输出编码。但要注意dangerouslySetInnerHTML和v-html,用了它们就等于绕过了框架的保护。
// 错误的做法:直接插入HTML
element.innerHTML = userInput; // 危险!
// 正确的做法:用textContent
element.textContent = userInput; // 安全
// 如果必须用innerHTML,先编码
function encodeHTML(str) {
return str.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
3.3 CSP策略配置:最后一道防线
CSP(内容安全策略)是个好东西。它就像给浏览器下了一道命令:「哪些资源可以加载,哪些不行」。就算攻击者注入了恶意脚本,CSP也能拦住它不让执行。
配置示例
// 通过HTTP头设置CSP
Content-Security-Policy:
default-src 'self';
script-src 'self' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
object-src 'none';
base-uri 'none';
这里有几个关键点:
default-src 'self':只信任同源资源script-src:严格控制脚本来源object-src 'none':禁止插件,Flash之类的别想了base-uri 'none':防止base标签劫持
我曾经踩过的坑:一开始把CSP设得太严,结果页面上的内联脚本全挂了,连Google Analytics都跑不了。后来才学会用'unsafe-inline'配合nonce或hash来精确控制。
Nonce vs Hash
如果你必须用内联脚本,别用'unsafe-inline',那等于把门打开了。用nonce或者hash更安全。
// 服务端生成nonce
const nonce = crypto.randomBytes(16).toString('base64');
res.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
// 页面中引用
<script nonce="${nonce}">
// 你的内联脚本
</script>
3.4 绕过案例:知己知彼
光知道怎么防还不够,得知道攻击者怎么绕。我整理了几个常见的绕过手法。
案例1:编码绕过
攻击者把<script>用URL编码、Unicode编码、甚至HTML实体编码混着来。
// 绕过简单过滤
<img src=x onerror="alert(1)">
// 上面这串编码解码后是 alert(1)
案例2:DOM clobbering
利用DOM元素的id或name覆盖全局变量。
<form id="config">
<input name="url" value="javascript:alert(1)">
</form>
<script>
// 如果代码里用了 config.url,就会被这个input覆盖
location.href = config.url; // 危险!
</script>
案例3:CSP绕过
如果CSP里允许了JSONP接口,攻击者可以利用它来执行任意代码。
// 假设CSP允许了 https://api.example.com
// 攻击者利用JSONP回调
<script src="https://api.example.com/jsonp?callback=alert(1)"></script>
防御要点:
- 永远不要信任用户输入,哪怕它经过了过滤
- 输出编码要针对上下文,别一刀切
- CSP要配置合理,别太松也别太紧
- 定期做安全审计,用工具扫描潜在漏洞
最后说一句,XSS防御没有银弹。输入过滤、输出编码、CSP这三层防线缺一不可。我见过太多项目只做了其中一层,结果被攻破后追悔莫及。嗯,安全这事儿,还是做全面点好。