跳到内容

点击劫持防御备忘单

简介

本备忘单旨在为开发人员提供关于如何防御点击劫持(又称 UI 整改攻击)的指导。

有三种主要的机制可用于防御这些攻击

  • 使用 X-Frame-Options内容安全策略 (frame-ancestors) HTTP 头,阻止浏览器在框架中加载页面。
  • 使用 SameSite cookie 属性,阻止页面在框架中加载时包含会话 cookie。
  • 在页面中实现 JavaScript 代码,尝试阻止其在框架中加载(称为“框架破坏者”)。

请注意,这些机制都是相互独立的,在可能的情况下,应实施多种机制以提供深度防御。

使用内容安全策略 (CSP) frame-ancestors 指令进行防御

frame-ancestors 指令可用于 Content-Security-Policy HTTP 响应头中,以指示浏览器是否应被允许在 <frame><iframe> 中渲染页面。网站可以使用此指令来避免点击劫持攻击,确保其内容不会被嵌入到其他网站中。

frame-ancestors 允许网站使用正常的内容安全策略语义授权多个域。

Content-Security-Policy: frame-ancestors 示例

CSP frame-ancestors 的常见用法

  • Content-Security-Policy: frame-ancestors 'none';
    • 这会阻止任何域对内容进行框架化。除非已确定有特定的框架需求,否则建议使用此设置。
  • Content-Security-Policy: frame-ancestors 'self';
    • 这只允许当前网站对内容进行框架化。
  • Content-Security-Policy: frame-ancestors 'self' *.somesite.com https://myfriend.site.com;
    • 这允许当前网站,以及 somesite.com 上的任何页面(使用任何协议),并且只允许 myfriend.site.com 页面,仅在默认端口 (443) 上使用 HTTPS。

请注意,selfnone 周围需要单引号,但其他源表达式周围可能不需要。

有关更多详细信息和更复杂的示例,请参阅以下文档

局限性

X-Frame-Options 优先: CSP 规范的“与 X-Frame-Options 的关系”部分指出:“如果资源以包含名为 frame-ancestors 的指令且其处置为“enforce”的策略交付,则必须忽略 X-Frame-Options 头”,但旧版浏览器(例如 Chrome 40 和 Firefox 35)忽略了此要求,而是遵循 X-Frame-Options 头。

浏览器支持

以下浏览器支持 CSP frame-ancestors。

参考资料

使用 X-Frame-Options 响应头进行防御

X-Frame-Options HTTP 响应头可用于指示浏览器是否应被允许在 <frame><iframe> 中渲染页面。网站可以使用此指令来避免点击劫持攻击,确保其内容不会被嵌入到其他网站中。为所有包含 HTML 内容的响应设置 X-Frame-Options 头。可能的值为“DENY”、“SAMEORIGIN”或“ALLOW-FROM uri”。

X-Frame-Options 头类型

X-Frame-Options 头有三种可能的值

  • DENY,阻止任何域对内容进行框架化。除非已确定有特定的框架需求,否则建议使用“DENY”设置。
  • SAMEORIGIN,只允许当前网站对内容进行框架化。
  • ALLOW-FROM uri,允许指定的“uri”对本页进行框架化。(例如,ALLOW-FROM http://www.example.com)。
    • 这是一个已废弃的指令,在现代浏览器中不再有效。
    • 请检查以下局限性,因为如果浏览器不支持,这将导致开放式失败。
    • 其他浏览器支持新的 CSP frame-ancestors 指令。少数浏览器两者都支持。

浏览器支持

以下浏览器支持 X-Frame-Options 头。

参考资料

实施

要实施此保护,您需要将 X-Frame-Options HTTP 响应头添加到您希望通过框架破坏来防止点击劫持的任何页面。一种方法是手动将 HTTP 响应头添加到每个页面。一个可能更简单的方法是实现一个过滤器,自动将头添加到每个页面,或在 Web 应用程序防火墙的 Web/应用服务器级别添加。

常见防御错误

尝试应用 X-Frame-Options 指令的 Meta 标签不起作用。例如,<meta http-equiv="X-Frame-Options" content="deny"> 将无效。您必须如上所述,将 X-FRAME-OPTIONS 指令作为 HTTP 响应头应用。

局限性

  • 每页策略规范:需要为每个页面指定策略,这会使部署复杂化。提供在整个站点(例如登录时)强制执行策略的能力,可以简化采用。
  • 多域网站的问题:当前实现不允许网站管理员提供允许框架化页面的域列表。虽然列出允许的域可能存在风险,但在某些情况下,网站管理员可能别无选择,只能使用多个主机名。
  • ALLOW-FROM 浏览器支持:ALLOW-FROM 选项已废弃,在现代浏览器中不再有效。请谨慎依赖 ALLOW-FROM。如果您应用它而浏览器不支持,那么您将没有任何点击劫持防御措施。
  • 不支持多重选项:无法同时允许当前站点和第三方站点对同一响应进行框架化。浏览器只识别一个 X-Frame-Options 头,并且该头只识别一个值。
  • 嵌套框架不适用于 SAMEORIGIN 和 ALLOW-FROM:在以下情况中,http://framed.invalid/child 框架无法加载,因为 ALLOW-FROM 适用于顶级浏览上下文,而不是直接父级。解决方案是在父框架和子框架中都使用 ALLOW-FROM(但这会阻止子框架加载,如果 //framed.invalid/parent 页面作为顶级文档加载)。

NestedFrames

  • X-Frame-Options 已废弃 尽管 X-Frame-Options 头受主流浏览器支持,但它已被 CSP Level 2 规范中的 frame-ancestors 指令所取代。
  • 代理 Web 代理以添加和剥离头部而闻名。如果 Web 代理剥离了 X-Frame-Options 头,那么网站就会失去其框架保护。

使用 SameSite Cookie 进行防御

RFC 6265bis 中定义的 SameSite cookie 属性主要旨在防御 跨站请求伪造 (CSRF);但它也可以提供针对点击劫持攻击的保护。

带有 strictlax SameSite 属性的 Cookie 不会被包含在对 <iframe> 内页面的请求中。这意味着,如果会话 cookie 被标记为 SameSite,任何需要受害者进行身份验证的点击劫持攻击都将无效,因为 cookie 不会被发送。Netsparker 博客上的一篇文章提供了关于在不同 SameSite 策略下哪些类型的请求会发送 cookie 的更多详细信息。

此方法在 JavaScript.info 网站上进行了讨论。

局限性

如果点击劫持攻击不需要用户进行身份验证,则此属性将不提供任何保护。

此外,虽然 大多数现代浏览器支持 SameSite 属性,但仍有一些用户(截至 2020 年 11 月约为 6%)的浏览器不支持它。

此属性的使用应被视为深度防御方法的一部分,不应作为防止点击劫持的唯一保护措施。

目前最佳的旧版浏览器框架打破脚本

防御点击劫持的一种方法是在不应被框架化的每个页面中包含一个“框架破坏者”脚本。以下方法将阻止网页被框架化,即使在不支持 X-Frame-Options-Header 的旧版浏览器中也能生效。

在文档的 HEAD 元素中,添加以下内容

首先给样式元素本身应用一个 ID

<style id="antiClickjack">
    body{display:none !important;}
</style>

然后,在脚本中紧接着通过其 ID 删除该样式

<script type="text/javascript">
    if (self === top) {
        var antiClickjack = document.getElementById("antiClickjack");
        antiClickjack.parentNode.removeChild(antiClickjack);
    } else {
        top.location = self.location;
    }
</script>

这样,所有内容都可以放在文档 HEAD 中,并且您的 API 中只需一个方法/标签库。

window.confirm() 保护

使用 X-Frame-Options 或框架破坏脚本是一种更保险的点击劫持保护方法。然而,在内容必须可被框架化的场景中,可以使用 window.confirm() 来帮助缓解点击劫持,通过告知用户他们即将执行的操作。

调用 window.confirm() 将显示一个无法被框架化的弹出窗口。如果 window.confirm() 源自与父级不同域的 iframe 中,则对话框将显示 window.confirm() 的来源域。在这种情况下,浏览器会显示对话框的来源,以帮助缓解点击劫持攻击。例如

<script type="text/javascript">
    var action_confirm = window.confirm("Are you sure you want to delete your youtube account?")
    if (action_confirm) {
        //... Perform action
    } else {
        //... The user does not want to perform the requested action.`
    }
</script>

不安全的无效脚本 切勿使用

考虑以下不建议用于防御点击劫持的代码片段

<script>if (top!=self) top.location.href=self.location.href</script>

这个简单的框架破坏脚本试图通过强制父窗口加载当前框架的 URL 来阻止页面被整合到框架或 iframe 中。不幸的是,多种击败这种类型脚本的方法已经公开。我们在此概述其中一些。

双重框架

一些框架破坏技术通过将值赋给 parent.location 来导航到正确的页面。如果受害页面由单个页面框架化,这种方法效果很好。然而,如果攻击者将受害者嵌套在一个框架内的另一个框架中(双重框架),那么由于子代框架导航策略,访问 parent.location 在所有流行浏览器中都会成为安全违规。这种安全违规会禁用反制导航。

受害者框架破坏代码

if(top.location != self.location) {
    parent.location = self.location;
}

攻击者顶级框架

<iframe src="attacker2.html">

攻击者子框架

<iframe src="http://www.victim.com">

onBeforeUnload 事件

用户可以手动取消框架页面提交的任何导航请求。为了利用这一点,框架页面会注册一个 onBeforeUnload 处理程序,该处理程序在框架页面因导航而即将卸载时被调用。处理程序函数返回一个字符串,该字符串将成为向用户显示的提示的一部分。

假设攻击者想框架化 PayPal。他注册了一个卸载处理程序函数,该函数返回字符串“您要退出 PayPal 吗?”。当此字符串显示给用户时,用户很可能会取消导航,从而击败 PayPal 的框架破坏尝试。

攻击者通过使用以下代码在顶部页面注册卸载事件来发动此攻击

<script>
    window.onbeforeunload = function(){
        return "Asking the user nicely";
    }
</script>

<iframe src="http://www.paypal.com">

PayPal 的框架破坏代码将生成一个 BeforeUnload 事件,激活我们的函数并提示用户取消导航事件。

无内容刷新

虽然之前的攻击需要用户交互,但同样的攻击可以在不提示用户的情况下完成。现代浏览器允许攻击者通过反复向响应“204 - 无内容”的网站提交导航请求,在 onBeforeUnload 事件处理程序中自动取消传入的导航请求。

导航到无内容站点实际上是一个空操作 (NOP),但它会刷新请求管道,从而取消原始导航请求。这是执行此操作的示例代码

var preventbust = 0
window.onbeforeunload = function() { killbust++ }
setInterval( function() {
    if(killbust > 0){
    killbust = 2;
    window.top.location = 'http://nocontent204.com'
    }
}, 1);
<iframe src="http://www.victim.com">

受限区域

大多数框架破坏依赖于框架页面中的 JavaScript 来检测框架并自我跳出。如果在子框架的上下文中禁用了 JavaScript,框架破坏代码将不会运行。不幸的是,有几种方法可以限制子框架中的 JavaScript

在 Chrome 中

<iframe src="http://www.victim.com" sandbox></iframe>

Firefox

在父页面中激活 designMode。虽然 designMode 在现代浏览器中仍然受支持,但其作为点击劫持攻击向量的有效性在当前浏览器版本中可能有所不同。

document.designMode = "on";