点击劫持攻击

点击劫持攻击就是恶意的页面 以用户的名义 点击受害网站。

原理

  1. 访问者被恶意页面吸引
  2. 页面上有一个看起来无害的链接(例如:点我,超好玩)
  3. 恶意页面在这个按钮下方有一个透明的<iframe>,这个 src来自于bilibili,这就使得“点赞”按钮恰好位于该链接下方。这是使用透明+ z-index实现的
  4. 用户点击这个链接,实际上点击的是点赞按钮

真实情况下点击后可能会损害用户的其他利益,这里用点赞代替。

示例

index.html 恶意网站

html
1<style>
2  iframe {
3    /* 来自受害网站的 iframe */
4    width: 400px;
5    height: 100px;
6    position: absolute;
7    top: 20px;
8    opacity: 0.5; /* 在实际中为 opacity:0 */
9    z-index: 1;
10  }
11</style>
12
13<div>点击即可变得富有:</div>
14
15<!-- 来自受害网站的 url -->
16<iframe src="bilibili.html" frameborder="0"></iframe>
17
18<button>点这里!</button>
19
20<div>……你很酷(我实际上是一名帅气的黑客)!</div>

bilibili.html 受害网站

html
1<input type="button" onclick="alert('你成功点了个赞!')" value="点我!" />

整体的样子是这样的

image-20220318142115322

上面的例子中,我们有一个半透明的<iframe src="bilibili.html" frameborder="0"></iframe>,它位于点击按钮之上。用户的点击实际上会点击到 iframe 上,但是用户不知情,因为 iframe 是透明的。

如果用户此时登陆了 bilibili,那么点击劫持攻击行为就成功了。(B 站需要登陆才能点赞)

点击劫持行为一般是对点击事件,而非键盘事件。

很简单的原因—— 因为 iframe 是不可见的,用户键入的内容会被隐藏,当用户发现输入的字符看不见时,通常会停止打字。

传统防御措施(弱)

最早的防御措施是一段禁止在 frame 中打开页面的 JavaScript 代码,它是这样写的:

javascript
1if (top != window) {
2  top.location = window.location
3}

意思是如果 window 发现它并不在顶部,那就顶部的 location 设置为自己的。

但是这种方法并不牢靠。

有多种方法可以让上面的代码无效,例如:

  • top 页面监听 beforeunload事件,阻止页面离开。

    javascript
    1window.onbeforeunload = function () {
    2  return false
    3}

    当 iframe 试图更改 top.location 时,访问者会收到一条消息,询问他们是否要离开页面。

    在大多数情况下,访问者会做出否定的回答,因为他们并不知道还有这么一个 iframe,他们所看到的只有顶级页面,他们没有理由离开。所以 top.location 不会变化!

  • Sandbox 特性

    sandbox 特性的限制之一就是导航。沙箱化的 iframe 不能更改 top.location。

    html
    1<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>

    上述代码中添加了允许脚本和允许表单的属性,但没添加allow-top-navigation,因此更改top.location是被禁止的。

有效的防御措施

服务端设置X-Frame-Options

服务端的 header 属性X-Frame-Options允许或禁止在 frame 中显示页面。

这个 header 包含 3 个值:

  • DENY

    始终禁止在 frame 中显示此页面

  • SAMEORIGIN

    允许在和父文档同源的 frame 中显示此页面

  • ALLOW-FROM domain

    允许在来自给定域的父文档的 frame 中显示此页面

例如,百度就是这样设置的

http
1X-Frame-Options:sameorigin

用一个 div 覆盖整个页面

用一个覆盖全屏的<div>拦截所有点击,只有确定不需要保护时,才移除<div>按钮

html
1<style>
2  #protector {
3    height: 100%;
4    width: 100%;
5    position: absolute;
6    left: 0;
7    top: 0;
8    z-index: 99999999;
9  }
10</style>
11
12<div id="protector">
13  <a href="/" target="_blank">前往网站</a>
14</div>
15
16<script>
17  // 如果顶级窗口来自其他源,这里则会出现一个 error
18  // 但是在本例中没有问题
19  if (top.document.domain == document.domain) {
20    protector.remove()
21  }
22</script>

Samesite cookie 特性

具有 samesite 特性的 cookie 仅在网站是通过直接方式打开(而不是通过 frame 或其他方式)的情况下才发送到网站。通过这种方式,即使用户点击了,cookie 也不会被发送出去,这样很多行为都会失效。

它可以这样设置:

http
1Set-Cookie: authorization=secret; samesite

当不使用 cookie 时,samesite cookie 特性将不会有任何影响。这可以使其他网站能够轻松地在 iframe 中显示我们公开的、未进行身份验证的页面。

唯一不好之处就是有些网站不使用 cookie 对用户身份进行验证,那还是会受到劫持攻击。

总结

点击劫持是一种诱骗用户在不知情的情况下点击恶意网站的方式。如果是重要的点击操作,这是非常危险的。

我们有三种有效的方法来防范这种攻击:

  • 设置X-Frame-Options: SAMEORIGIN不让非同源的网站用 frame 嵌入
  • 用一个 div 覆盖,只有满足条件才删除这个 div,这样用户就没办法点到 frame 中的按钮了
  • 通过samesit特性,仅在网站通过直接方式打开的(而不是通过 frame 或其他方式)的情况下才发送 cookie,这样即使用户点击到也是无效的操作,这种方法仅适用于用 cookie 做身份验证的网站。