跨站泄露备忘录¶
简介¶
本文介绍了针对跨站泄露漏洞(XS Leaks)的攻击和防御示例。由于此漏洞基于现代 Web 浏览器的核心机制,因此也被称为浏览器侧信道攻击。XS-Leaks 攻击旨在利用在网站间跨站通信中交换的看似微不足道的信息。这些信息推断出关于受害者用户账户的先前问题的答案。请查看以下示例
- 用户当前是否已登录?
- 用户 ID 是 1337 吗?
- 用户是管理员吗?
- 用户的联系人列表中是否有特定电子邮件地址的人?
基于此类问题,攻击者可能会尝试根据应用程序的上下文推断出答案。在大多数情况下,答案将是二进制形式(是或否)。此漏洞的影响在很大程度上取决于应用程序的风险配置文件。尽管如此,XS Leaks 可能会对用户隐私和匿名性构成真正的威胁。
攻击向量¶
- 整个攻击发生在受害者的浏览器端——就像 XSS 攻击一样
- 在某些情况下,受害者必须在攻击者的网站上停留更长时间才能使攻击成功。
同源策略 (SOP)¶
在描述攻击之前,最好先了解浏览器中最重要的安全机制之一——同源策略。以下是几个关键方面
- 如果两个 URL 的协议、端口和主机都相同,则它们被认为是同源的
- 任何源都可以向另一个源发送请求,但由于同源策略,它们将无法直接读取响应
- 同源策略可以通过跨域资源共享 (CORS) 放宽。
源 A | 源 B | 同源吗? |
---|---|---|
https://example.com |
http://sub.example.com |
否,主机不同 |
https://example.com |
https://example.com:443 |
是!源 A 中隐式端口 |
尽管 SOP 原则保护我们免于在跨域通信中访问信息,但基于残余数据的 XS-Leaks 攻击仍然可以推断出一些信息。
SameSite Cookies¶
Cookie 的 SameSite 属性告诉浏览器是否应在来自其他站点的请求中包含该 Cookie。SameSite 属性接受以下值
None
- Cookie 将附加到来自其他站点的请求中,但必须通过安全的 HTTPS 通道发送Lax
- 如果请求方法是 GET 且请求是针对顶级导航(即导航改变了浏览器顶部地址栏中的地址),则 Cookie 将附加到来自其他页面的请求中Strict
- Cookie 永远不会从其他站点发送
值得一提的是,基于 Chromium 的浏览器会将默认未设置 SameSite 属性的 Cookie 视为 Lax。
SameSite Cookie 是一种强大的纵深防御机制,可防御某些类别的 XS Leaks 和 CSRF 攻击,这可以显著减少攻击面,但可能无法完全阻止它们(例如,参见 基于窗口的 XS Leaks 攻击,如 帧计数 和 导航)。
我们如何判断两个站点是否为 SameSite?¶
在 SameSite 属性的上下文中,我们将站点视为 TLD(顶级域)及其之前的域名组合。例如
完整 URL | 站点 (eTLD+1) |
---|---|
https://example.com:443/data?query=test |
example.com |
我们为什么要讨论 eTLD+1 而不仅仅是 TLD+1?这是因为像 .github.io
或 .eu.org
这样的域。这些部分不够原子化,无法很好地进行比较。因此,创建了一个“有效”TLD(eTLD)列表,可以在这里找到。
具有相同 eTLD+1 的站点被认为是 SameSite,例如
源 A | 源 B | SameSite 吗? |
---|---|---|
https://example.com |
http://example.com |
是,方案不重要 |
https://evil.net |
https://example.com |
否,eTLD+1 不同 |
https://sub.example.com |
https://data.example.com |
是,子域不重要 |
有关 SameSite 的更多信息,请参阅这篇出色的文章:理解“same-site”。
使用元素 ID 属性的攻击¶
DOM 中的元素可以拥有在文档中唯一的 ID 属性。例如
<button id="pro">Pro account</button>
如果我们向 URL 追加哈希值,例如 https://example.com#pro
,浏览器将自动聚焦到具有给定 ID 的元素上。此外,JavaScript focus 事件会被触发。攻击者可能会尝试在自己控制的页面上,将具有特定源的应用程序嵌入到 iframe 中
然后在主文档中添加对 blur 事件(与 focus 相反)的监听器。当受害者访问攻击者的网站时,blur 事件会被触发。攻击者将能够推断出受害者拥有专业账户。
防御¶
框架保护¶
如果您不需要其他源将您的应用程序嵌入到框架中,可以考虑使用以下两种机制之一
- 内容安全策略 frame-ancestors 指令。阅读更多关于语法的信息。
- X-Frame-Options - 主要用于支持旧浏览器。
设置框架保护可以有效地阻止在攻击者控制的源上将您的应用程序嵌入到框架中,并防止其他攻击,例如 点击劫持。
Fetch 元数据 (Sec-Fetch-Dest)¶
Sec-Fetch-Dest 头部为我们提供了关于请求最终目标的信息。此头部由浏览器自动包含,并且是 Fetch 元数据标准中的一个头部。
使用 Sec-Fetch-Dest,您可以构建有效的自定义资源隔离策略,例如
app.get('/', (req, res) => {
if (req.get('Sec-Fetch-Dest') === 'iframe') {
return res.sendStatus(403);
}
res.send({
message: 'Hello!'
});
});
如果您想使用 Fetch 元数据标准中的头部,请确保您的用户浏览器支持此标准(您可以在这里查看)。此外,如果请求中不包含 Sec-Fetch-* 头部,请考虑在代码中使用适当的备用方案。
基于错误事件的攻击¶
通常允许嵌入来自其他源的资源。例如,您可以在页面上嵌入来自其他源的图像甚至是脚本。但由于 SOP 策略,不允许读取跨源资源。
当浏览器发送资源请求时,服务器会处理该请求并决定响应(例如 200 OK 或 404 NOT FOUND)。浏览器接收到 HTTP 响应后,会根据响应触发相应的 JavaScript 事件(onload 或 onerror)。
通过这种方式,我们可以尝试加载资源,并根据响应状态推断它们在已登录受害者上下文中是否存在。让我们看看以下情况
GET /api/user/1234
- 200 OK - 当前登录用户是 1234,因为我们成功加载了资源(onload 事件触发)GET /api/user/1235
- 401 Unauthorized - 1235 不是当前登录用户的 ID(onerror 事件将被触发)
根据以上示例,攻击者可以在其控制的源上使用 JavaScript,通过简单的循环遍历所有值来猜测受害者的 ID。
function checkId(id) {
const script = document.createElement('script');
script.src = `https://example.com/api/users/${id}`;
script.onload = () => {
console.log(`Logged user id: ${id}`);
};
document.body.appendChild(script);
}
// Generate array [0, 1, ..., 40]
const ids = Array(41)
.fill()
.map((_, i) => i + 0);
for (const id of ids) {
checkId(id);
}
请注意,即使由于浏览器中强大的隔离机制(例如 跨域资源阻止),攻击者也无法读取响应正文,但他并不关心这一点。他所需要的只是在 onload
事件触发时收到的成功信息。
防御¶
子资源保护¶
在某些情况下,可以实施特殊唯一令牌机制来保护我们的敏感端点。
/api/users/1234?token=be930b8cfb5011eb9a030242ac130003
- 令牌应该长且唯一
- 后端必须正确验证请求中传递的令牌
尽管它非常有效,但该解决方案在正确实施时会产生显著的开销。
Fetch 元数据 (Sec-Fetch-Site)¶
此头部指定请求的来源,它接受以下值
跨站点
同源
同站点
none
- 用户直接访问页面
与 Sec-Fetch-Dest 类似,此头部由浏览器自动附加到每个请求中,并且是 Fetch 元数据标准的一部分。使用示例
app.get('/api/users/:id', authorization, (req, res) => {
if (req.get('Sec-Fetch-Site') === 'cross-site') {
return res.sendStatus(403);
}
// ... more code
return res.send({ id: 1234, name: 'John', role: 'admin' });
});
跨域资源策略 (CORP)¶
如果服务器返回具有适当值的此头部,浏览器将不会在其他应用程序中加载来自我们站点或源的资源(即使是静态图像)。可能的值
同站点
同源
跨域
在此处阅读更多关于 CORP 的信息:here。
对 postMessage 通信的攻击¶
有时在受控情况下,我们希望即使有 SOP 也能在不同源之间交换信息。我们可以使用 postMessage 机制。请参阅下面的示例
// Origin: http://example.com
const site = new URLSearchParams(window.location.search).get('site'); // https://evil.com
const popup = window.open(site);
popup.postMessage('secret message!', '*');
// Origin: https://evil.com
window.addEventListener('message', e => {
alert(e.data) // secret message! - leak
});
防御¶
指定严格的 targetOrigin¶
为了避免像上述情况一样,攻击者设法获取窗口引用以接收消息,请务必在 postMessage 中指定精确的 targetOrigin
。将通配符 *
传递给 targetOrigin
会导致任何源都能接收到消息。
// Origin: http://example.com
const site = new URLSearchParams(window.location.search).get('site'); // https://evil.com
const popup = window.open(site);
popup.postMessage('secret message!', 'https://sub.example.com');
// Origin: https://evil.com
window.addEventListener('message', e => {
alert(e.data) // no data!
});
帧计数攻击¶
关于窗口中已加载帧数量的信息可能是泄露的来源。以一个将搜索结果加载到帧中的应用程序为例,如果结果为空,则该帧不会出现。
攻击者可以通过计算 window.frames
对象中的帧数量来获取窗口中已加载帧数量的信息。
因此,最终攻击者可以获取电子邮件列表,并在一个简单的循环中,打开后续窗口并计算帧的数量。如果打开窗口中的帧数量等于 1,则该电子邮件存在于受害者使用的应用程序的客户端数据库中。
防御¶
跨域打开者策略 (COOP)¶
设置此头部将阻止跨源文档在相同的浏览上下文组中打开。此解决方案确保文档 A 打开另一个文档时无法访问 window
对象。可能的值
unsafe-none
same-origin-allow-popups
同源
如果服务器返回例如 same-origin
COOP 头部,攻击将失败
const win = window.open('https://example.com/admin/customers?search=john%40example.com');
console.log(win.frames.length) // Cannot read property 'length' of null
使用浏览器缓存的攻击¶
浏览器缓存有助于显著减少页面再次访问时的加载时间。然而,它也可能带来信息泄露的风险。如果攻击者能够在加载时间后检测资源是否从缓存中加载,他将能够据此得出一些结论。
原理很简单,从缓存内存加载的资源将比从服务器加载快得多。
攻击者可以在其网站上嵌入一个只有管理员角色的用户才能访问的资源。然后,利用 JavaScript 读取特定资源的加载时间,并根据此信息推断该资源是否在缓存中。
// Threshold above which we consider a resource to have loaded from the server
// const THRESHOLD = ...
const adminImagePerfEntry = window.performance
.getEntries()
.filter((entry) => entry.name.endsWith('admin.svg'));
if (adminImagePerfEntry.duration < THRESHOLD) {
console.log('Image loaded from cache!')
}
防御¶
图像的不可预测令牌¶
当用户希望资源仍然被缓存,但攻击者无法得知时,此技术是准确的。
/avatars/admin.svg?token=be930b8cfb5011eb9a030242ac130003
- 令牌在每个用户的上下文中都应该是唯一的
- 如果攻击者无法猜测此令牌,他就无法检测资源是否从缓存加载。
使用 Cache-Control 头部¶
如果您接受因用户每次访问站点都必须从服务器重新加载资源而导致的性能下降,则可以禁用缓存机制。要禁用您希望保护的资源的缓存,请设置响应头部 Cache-Control: no-store
。
快速建议¶
- 如果您的应用程序使用 Cookie,请确保设置适当的 SameSite 属性。
- 考虑您是否真的希望允许您的应用程序嵌入到框架中。如果不是,请考虑使用框架保护部分中描述的机制。
- 为了加强您的应用程序与其他源之间的隔离,请使用具有适当值的 跨域资源策略 和 跨域打开者策略 头部。
- 使用 Fetch 元数据中可用的头部来构建您自己的资源隔离策略。