跨站泄露 (XS-Leaks)
跨站泄露(也称为 XS-Leaks)是一类攻击,攻击者的网站可以通过利用允许网站之间交互的 Web 平台 API,获取有关目标网站或用户与目标网站之间关系的信息。泄露的信息可能包括,例如:
- 用户是否访问过目标网站。
- 用户是否已登录目标网站。
- 用户在该网站上的 ID 是什么。
- 用户最近在该网站上搜索过什么。
这可能看起来比例如跨站脚本攻击造成的危害要小得多,但它仍然可能对用户造成严重后果。例如:
- 用户可能在不希望公开的网站上拥有账户。将这些信息泄露给攻击者可能会使他们面临敲诈或来自压迫性政府的报复(例如,针对寻求特定医疗程序信息的用户)。
- 知道用户在某个网站上拥有账户,尤其是如果可以确定他们的用户 ID,可以使后续的钓鱼攻击更具说服力。
与其他攻击(例如 XSS 或 点击劫持)不同,跨站泄露不是单一的技术。相反,它们是利用浏览器隔离网站方式中的弱点的整类攻击的统称。
在本指南中,我们不会尝试描述所有的跨站泄露攻击和防御。相反,我们将首先描述一些示例攻击,然后概述导致这些攻击的常见底层弱点,最后描述一些可以抵御许多已知攻击的通用防御措施。
跨站泄露示例
在本节中,我们将描述三种不同的跨站泄露,以了解它们的工作原理。
- 使用错误事件泄露页面存在:在此攻击中,攻击者可以通过尝试将目标网站中的特定端点作为资源加载,并监听
error和load事件,来确定它们是否返回 HTTP 错误代码。如果某些页面仅对登录用户可用,攻击者可以确定用户是否已登录目标网站。 - 使用窗口引用进行帧计数:在此攻击中,攻击者获取托管目标网站中页面的
window对象的引用,例如作为对window.open()调用返回的值。攻击者随后可以确定目标页面中<iframe>元素的数量,这可能再次揭示用户是否已登录目标网站。 - 使用 CSP 泄露重定向:在此攻击中,攻击者的页面具有一个 内容安全策略 (Content Security Policy),该策略只允许加载目标网站中的特定页面,然后尝试加载该页面。如果页面加载被阻止,攻击者就知道目标重定向了请求。此重定向可能根据网站的工作方式指示用户是否已登录(或未登录)。
所有三种攻击都以相同的方式部署:攻击者制作一个实现攻击的页面,然后说服用户访问该页面,例如通过向他们发送包含链接的电子邮件或分享帖子。当用户访问该页面时,攻击会自动执行。
在本节的其余部分,我们将更详细地描述这三种攻击,以便您具体了解它们的工作原理。尽管这三种攻击针对 Web 平台的完全不同部分,但它们有一个共同的根本原因:浏览器通过框架、加载子资源或打开新窗口等机制使网站相互连接和交互的程度。
注意: 有关更完整的跨站泄露目录,请参阅 XS-Leaks Wiki 和 OWASP 跨站泄露备忘单。
使用错误事件泄露页面存在
在此攻击中,攻击者通过观察尝试将特定页面作为资源嵌入是否会产生错误,来测试目标网站中的特定页面是否可以加载。如果这些页面仅对登录用户可用,攻击者可以确定用户是否已登录。
此攻击依赖于网站从另一个网站加载资源的能力,例如通过将 <script> 元素的 src 属性设置为资源的 URL:
const script = document.createElement("script");
script.src = "https://example.org/admin";
document.head.appendChild(script);
这会导致向 https://example.org/ 网站发出 HTTP 请求。如果请求包含网站用于识别用户的 cookie,并且请求的页面仅对登录用户可用,那么请求的成功或失败会揭示用户是否已登录。
如果请求失败,例如因为服务器返回 HTTP 404 状态码,则元素会触发 error 事件。如果请求成功,则元素会触发 load 事件。通过监听这些事件,攻击者可以发现用户是否已登录。
const url = "https://example.org/admin";
const script = document.createElement("script");
script.addEventListener("load", (e) => {
console.log(`${url} exists`);
});
script.addEventListener("error", (e) => {
console.log(`${url} does not exist`);
});
script.src = url;
document.head.appendChild(script);
攻击者甚至可以通过迭代尝试加载页面,查看是否存在类似 https://example.org/users/my_username 的页面,来发现用户的 ID。
使用窗口引用进行帧计数
在帧计数攻击中,攻击者发现目标页面中当前加载的帧数。反过来,这会泄露有关目标网站状态的信息,这可能使攻击者能够了解例如用户当前是否已登录该网站。
如果攻击者网站获取了包含目标网站的 Window 对象的引用,攻击者可以通过读取 window.length 属性来计算目标网站中的帧数。
攻击者可以通过调用 window.open() 获取 Window 对象:
const target = window.open("https://example.org");
const frames = target.length;
或者,攻击者可以将目标网站嵌入到 <iframe> 中,并检索帧的 contentWindow 属性:
<iframe src="https://example.org"></iframe>
const target = document.querySelector("iframe").contentWindow;
const frames = target.length;
使用 CSP 泄露重定向
在某些网站中,服务器会根据用户是否已登录(或在该网站上具有某种特殊状态)来重定向请求,或不重定向。例如,假设有一个网站,管理员可以在 https://admin.example.org/ 看到一个页面。如果用户未登录并请求此页面,则服务器可能会将他们重定向到 https://login.example.org/。这意味着如果攻击者可以确定尝试加载 https://admin.example.org/ 是否导致重定向,那么他们就知道用户是否是该网站的管理员。
在此描述的攻击中,攻击者使用 内容安全策略 (CSP) 功能来检测跨站请求是否被重定向。
-
首先,他们创建一个由 CSP 管理的页面,该 CSP 只允许
<iframe>元素包含来自https://admin.example.org/的内容。 -
接下来,他们在页面中添加一个事件监听器,监听
securitypolicyviolation事件。 -
最后,他们创建一个
<iframe>元素并将其src属性设置为https://admin.example.org/。
<!doctype html>
<html lang="en-US">
<head>
<meta
http-equiv="Content-Security-Policy"
content="frame-src https://admin.example.org/" />
</head>
<body>
<script>
document.addEventListener("securitypolicyviolation", () => {
console.log("Page was redirected");
});
const frame = document.createElement("iframe");
document.body.appendChild(frame);
frame.src = "https://admin.example.org/";
</script>
</body>
</html>
- 如果用户以管理员身份登录,则
<iframe>将加载,浏览器不会触发securitypolicyviolation事件。 - 如果用户未以管理员身份登录,服务器会重定向到
https://login.example.org/。由于此 URL 不受攻击者 CSP 的允许,浏览器将阻止<iframe>并触发securitypolicyviolation事件,攻击者的事件处理程序将运行。
请注意,即使目标网站使用诸如 frame-ancestors 等机制禁止嵌入,此攻击也有效。
跨站泄露防御
跨站泄露利用 Web 平台中允许网站相互交互的机制。相应地,防御跨站泄露通常涉及通过禁用或控制这些跨站交互来将目标网站与潜在攻击者隔离。
由于跨站泄露可以通过多种不同方式工作,因此没有单一的防御措施可以抵御所有这些泄露。但是,有几种实践可以抵御其中许多泄露,我们在此将它们总结如下。
Fetch 元数据
Fetch 元数据是用于表示一组 HTTP 请求头的术语,这些请求头提供有关 HTTP 请求上下文的信息,包括:
Sec-Fetch-Site:请求是否为同源、同站或跨站。Sec-Fetch-Mode:请求的mode。Sec-Fetch-User:请求是否是用户发起的导航。Sec-Fetch-Dest:请求的destination。
Fetch 元数据头本身并不是防御机制,但它使服务器能够实现一项策略,该策略将拒绝用于跨站泄露以及其他攻击(例如 跨站请求伪造 (CSRF) 攻击)的请求。
例如,使用错误事件泄露页面存在攻击依赖于攻击者能够发出跨站请求,将属于目标的页面作为资源加载:
// Attempt to load a page in the target as a resource
const script = document.createElement("script");
script.src = "https://example.org/admin";
document.head.appendChild(script);
服务器可以使用 Fetch 元数据拒绝这些请求,如以下 Express 代码所示:
function isAllowed(req) {
// Allow same-origin, same-site, and user-initiated requests
const secFetchSite = req.headers["sec-fetch-site"];
if (
secFetchSite === "same-origin" ||
secFetchSite === "same-site" ||
secFetchSite === "none"
) {
return true;
}
// Allow cross-site navigations, such as clicking links
const secFetchMode = req.headers["sec-fetch-mode"];
if (secFetchMode === "navigate" && req.method === "GET") {
return true;
}
// Deny everything else
return false;
}
app.get("/admin", (req, res) => {
res.setHeader("Vary", "sec-fetch-site, sec-fetch-mode");
if (isAllowed(req)) {
// Respond with the admin page if the user is admin
getAdminPage(req, res);
} else {
res.status(404).send("Not found.");
}
});
由于攻击者的请求是跨站的且不是导航,因此无论用户是否登录,此服务器始终会返回错误。
请注意,我们还发送了 Vary 响应头。这确保如果响应被缓存,缓存的响应将仅提供给 Fetch 元数据头具有相同值的请求。
这样的策略称为资源隔离策略。要了解更多关于使用 Fetch 元数据实现隔离策略的信息,请参阅 使用 Fetch 元数据保护您的资源免受 Web 攻击 和 隔离策略。
SameSite cookies
SameSite cookie 属性决定了 cookie 是否会在源自不同站点的请求中发送。
SameSite 的 Lax 值表示跨站请求只有在请求是顶级导航(基本上意味着浏览器地址栏中的值更改为目标站点)并且使用 安全 方法(最值得注意的是,这排除了 POST 请求)时才会包含 cookie。
这可以防止一些跨站泄露。例如,使用错误事件泄露页面存在攻击依赖于攻击者发出包含用户会话 cookie 的跨站资源请求。将用户会话 cookie 上的 SameSite 设置为 Lax 将阻止此攻击,因为 cookie 不会包含在攻击者的请求中,并且永远不会返回需要登录的页面。
通常,SameSite 应被视为一种纵深防御措施,应与更明确的隔离策略(例如基于 Fetch 元数据的策略)一起部署。
框架保护
许多跨站泄露依赖于攻击站点能够将目标嵌入为 <iframe>。例如,这是攻击者获取目标 window 引用的一种方法,以实现帧计数攻击。
这意味着,除非您需要允许嵌入,否则阻止站点被嵌入是一种良好的实践;如果您确实需要允许嵌入,则应尽可能限制它。
这里有两个相关的工具:
- 内容安全策略中的
frame-ancestors指令。 X-Frame-Options响应头。
frame-ancestors 指令是 X-Frame-Options 的替代品。尽管 浏览器对 frame-ancestors 的支持非常好,但一些非常老的浏览器,特别是 Internet Explorer,不支持 frame-ancestors。
如果同时设置了 frame-ancestors 和 X-Frame-Options,那么支持 frame-ancestors 的浏览器将忽略 X-Frame-Options。这意味着没有理由不同时设置 X-Frame-Options 和 frame-ancestors,从而即使在不支持 frame-ancestors 的浏览器中也能防止嵌入。
跨域打开策略 (COOP)
正如我们在帧计数攻击中看到的那样,获取目标 window 对象的另一种方法是作为对 window.open() 调用的返回值:
const target = window.open("https://example.com");
Cross-Origin-Opener-Policy 响应头决定文档是否将在与其打开它的文档相同的 浏览上下文组 中打开。
如果您的服务器发送此头并将其设置为除默认值 "unsafe-none" 之外的任何值,那么如果来自不同源的文档尝试使用 window.open() 打开您的页面,您的页面将加载到不同的浏览上下文组中。除其他外,这意味着打开者将无法获取到您的页面的 window 对象的引用,因此无法在帧计数攻击中使用它。
防御总结清单
跨站泄露包括针对 Web 平台不同部分的各种攻击。单一的防御措施无法抵御所有这些攻击,而且一些泄露,例如利用 CSP 泄露重定向的泄露,目前还没有任何防御措施。
在本指南中,我们概述了一些有助于将您的网站与潜在攻击者隔离的防御措施,我们建议您全部实施:
- 使用 Fetch 元数据实施资源隔离策略。
- 如果可能,将会话 cookie 的
SameSite属性设置为Strict;如果必要,则设置为Lax。 - 使用
frame-ancestorsCSP 指令 和X-Frame-Options响应头来防止您的网站被嵌入,或控制哪些网站可以嵌入您的网站。 - 发送
Cross-Origin-Opener-Policy响应头,以防止其他网站访问您的window全局对象。
另见
- XS-Leaks Wiki (xsleaks.dev)
- 跨站泄露备忘单 (OWASP)