クリックジャッキング脆弱性をこの目で確かめてx-frame-optionsヘッダを設定した

# TL;DR

株式会社LIFULLの2年目Webエンジニア、@mejilebenです。
今回はクリックジャッキング脆弱性について、実際に手元で罠サイトを構築して動作を確かめた上で、脆弱性対策をした話を書きます。
非常に初歩的すぎる話なのですが、本を読むだけで脆弱性分かった気になるんじゃなくて、この目で確かめる大切さを実感したので記事に残します。

# クリックジャッキング脆弱性とは
自分のサイトがいつの間にか罠サイトの中に「iframe」で埋め込まれてしまった結果、罠サイトに来訪したユーザーによって意図しないうちに自分のサイトが操作されてしまうような脆弱性。

# 再現してみよう

## 脆弱性サイトの構築

脆弱性を検証したいサイト（自分が開発中のサイト）のURLを用意する

条件として、ログイン／ログアウト処理を実装しており、ログイン済みの状態でヘッダ部分のログアウトボタンを推せば、すぐにログアウトできる状態にしておいて下さい。

今回は会社の研修で作成した音楽口コミサイト「Rebeat」を使います。研修で作成したのでまだ脆弱性対策が不十分でした。
ここではURLは””http://test.rebeat.be ””とします。

<img width="456" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/156567/071d998d-cfae-3a54-7040-b66912654036.png">


## 罠サイトの構築

- 罠サイトにボタンと、魅惑的な文章を用意する。思わずボタンを押したくなっちゃうような文章を考えてみて下さい。

```html:step3.html
    <h1>iPhoneのキャンペーンに当選しました！</h1>
    <span>これは冗談ではありません！あなたは全体の1％しか当選しないキャンペーンに当選しました！今すぐ試用するボタンを押して、新しく先鋭的で最新のiPhone7を利用しましよう！</span>
    <input class="button" type="button" value="試用する！" onclick="alert('登録しました')">
```

![image2018-3-15_12-46-16.png](https://qiita-image-store.s3.amazonaws.com/0/156567/00b6776f-e3c6-97f4-675f-f7176b97bac1.png)

- iframeを使って罠サイトに「Rebeat」を埋め込むコードを用意する

```html:step2.html
    <iframe id="zeijakusei" height=500 width=1000 frameborder="0" src="http://test.rebeat.be"></iframe>
```

- 「罠サイトに配置したボタン」と「脆弱性を検証したいサイトの中で、何らかの操作をするボタン」の位置を合わせます。「Rebeat」をiframeで埋め込み、ログアウトボタンの位置を合わせました。

```html:step4.html
        #zeijakusei {
            position: absolute;
            top: 230px;
            left: 0px;

            filter: alpha(opacity=0);
            -moz-opacity: 0.5;
            opacity: 0.5;
            z-index: 10;
        }
```

![image2018-3-15_12-50-20.png](https://qiita-image-store.s3.amazonaws.com/0/156567/5f3e64ca-1ea9-9e84-8762-7147d0eb2772.png)

今は透明度で0.5を指定しているので、若干透明で重なっている状態になっています。
綺麗に「試用する」ボタンと、「Rebeat」の「ログアウト」ボタンが重なっています。

- 透明度を100％にして、罠サイトを完全に普通のサイトに見えるようにする。opacity = 0にする。

これにて完成です。

## 罠の実践
### 準備

埋め込んだ側のサイトに行って、ログイン処理を済ませておきましょう。

### 実践

あなたは普通にGoogle検索から罠サイトに到達しました。もしくは怪しげな広告を間違って踏んでしまってこのサイトに来ました。
![image2018-3-15_12-46-16.png](https://qiita-image-store.s3.amazonaws.com/0/156567/b14ac9df-4dee-7448-b596-40f35516c28f.png)

欲しいですね、最新のiPhone7。思わずあなたは「試用する」ボタンをクリックしてしまいました。

その後、結局iPhone7を手に入れられなくて悔しがるあなたは、気を紛らわせるために音楽口コミサイト「Rebeat」を訪れます。

<img width="818" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/156567/58d0a02e-d4e0-c77d-ba6d-ad75038beb7d.png">

すると、いつの間にかログアウトされてしまっていました。

これにて検証完了です。今回はログアウトですけど、例えば他アカウントへのフォローボタンをクリックジャッキングすれば、スパムアカウントのフォロワー数増加に悪用されるなどの可能性があります。

# クリックジャッキングの原因
他ドメインのサイトが、iframeであなたのサイトを読み込めてしまうのが原因です。

もう少し言うと、「iframeで読み込み可能なページに、更新系の処理が実装されていること」が根本原因です。

iframeで他のサイトを埋め込むこと自体は別に悪いことではなく、例えばWebページ上でYouTube動画を再生できるような仕組みはiframeで構成されています。
[YouTubeドキュメント](https://developers.google.com/youtube/player_parameters?hl=ja#Manual_IFrame_Embeds)

なので、正確には「iframeで読み込み可能なページに、更新系の処理が実装されていること」が根本原因であり、クリックジャッキング対策をするときはサービス要件に応じて、ページごとに対策をする必要がある場合もあります。

# クリックジャッキング対策

X-Frame-Optionsヘッダに、「SAMEORIGIN」もしくは「DENY」を設定しましょう。
[MDNドキュメント](https://developer.mozilla.org/ja/docs/Web/HTTP/X-Frame-Options)
NginxやApacheといったWebサーバーの設定で対策するのが一般的かと思います。

例えばYouTubeの事例で言うと、[トップページ](https://www.youtube.com/)ではクリックジャッキング対策がなされています。

<img width="469" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/156567/f015a164-8d6b-5469-cb7e-76934edc7b2b.png">

しかし、[iframe埋め込み用のページ](https://www.youtube.com/embed/M7lc1UVf-VE)ではX-Frame-Optionsヘッダが設定されていないことが分かります。
でも、例えばいいねボタンなどの更新系の処理が無いので、クリックジャッキング脆弱性は無いといえます。

<img width="488" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/156567/19ab5e25-4c09-cb52-4933-0c69550f4d85.png">

以上です。

# 結論
サービス要件に応じてX-Frame-Optionsヘッダに、「SAMEORIGIN」もしくは「DENY」を設定しましょう。
更新系の処理があり得るページでは、原則iframeで利用される用途を想定しないようにしましょう。

セキュリティはアプリ側だけでなくHTTP的な部分も見ていく必要があるので奥が深いですね。
世の中優しい人間ばかりになったらいいのに。

## 余談：Firebaseでの対策
余談ですが、僕は個人でFirebaseをバックエンドにVue.jsでSPAのWebアプリケーション開発をしていまして、そこでの対策も実践したので共有します。
firebase.jsonに設定してあげればいいです。
ただしもしページごとに設定を変えたい場合はsourceの部分を特定のパスに書き換えた設定も書くべきでしょう。

```json:firebase.json
    "headers": [
      {
        "source": "**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "private, no-store, no-cache, must-revalidate"
          },
          {
            "key": "X-Frame-Options",
            "value": "DENY"
          },
          (〜中略〜)
          {
            "key": "X-XSS-Protection",
            "value": "1; mode=block"
          },
          {
            "key": "X-Content-Type-Options",
            "value": "nosniff"
          }
        ]
      }
    ]
```