Lambda@Edgeを使ってX-Frame-Optionsヘッダを追加してみた

こんにちは。カトアキです。

Amazon S3とCloudFrontを使って公開しているWEBサイトにLambda@Edgeを使って独自ヘッダを付与する方法の紹介です。 ユースケースとしてはクリックジャッキング対策を想定して、X-Frame-Optionsヘッダを追加することを試してみましたのでその点について具体的に記述します。

クリックジャッキングとは

利用者を騙すためのボタンやiframe等を使って、サイト運営者側が意図するブラウザの操作を行わせることです。

例を挙げるとキリがありませんが、次のような操作があげられます。

  • 掲示板へ書き込みをさせる
  • 広告ページのクリックをさせる
  • SNSの「いいね」系ボタンのクリックをさせる

仕組みについてです。 例えばSNSの「いいね」ボタンをクリックさせるサイトの場合はこのような形になります。

これ、そもそも別サイトのクリックをさせられていることにも気づかないかもしれないですよね。怖い。。

X-Frame-Optionsヘッダについて

先の「クリックさせたいページ」として自分のWEBサイトが使われてしまうと困りますよね。

こういったケースに対しては「X-Frame-Options」というヘッダを付与してiframeによる外部からの読み込みを禁止することで、未然に防ぐことができます。

参考にしたサイト:X-Frame-Options - HTTP | MDN

やったこと

ということで、この記事では以下を試しました。

  • Webページの作成/Amazon S3の設定/CloudFrontの設定
    • iframeで呼び出されるページと呼び出すページを用意
    • iframeで呼び出されたページのボタンがクリックできることを確認
  • Lambdaの設定
    • iframeで呼び出されるページにX-Frame-Optionsヘッダが付与される設定を行う
  • 動作確認
    • iframeで呼び出されたページのボタンがクリックできないことを確認

Webページの作成

こんなWEBページを作成します。 fuga.htmlを開いて、本来何も起こらないはずの「ふがふがボタン」のエリアをクリックしたらダイアログが出てしまう(実際は「ほげほげボタン」が押される)というものです。 「ほげほげボタン」ページ(hoge.html)

 
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
        }
        div {
            height: 100vh;
            width: 100vw;
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
</head>
<body>
    <div>
        <button onClick="(alert('ほげほげ'))">ほげほげボタン</button>
    </div>
</body>
</html>

「ふがふがボタン」ページ(fuga.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body{
            margin: 0;
        }
        div {
            height: 100vh;
            width: 100vw;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        iframe {
            position: absolute;
            height: 100%;
            width: 100%;
            top: 0;
            left: 0;
            opacity: 0;
        }
    </style>
</head>
<body>
    <div>
        <button>ふがふがボタン</button>
    </div>
    <iframe src="./hoge.html" frameborder="0"></iframe>
</body>
</html>

Amazon S3の設定

作成したhtmlAmazon S3上で公開します。動作確認用のバケットを用意して、前述のhoge.htmlfuga.htmlを格納し、それらにパブリック読み取りアクセス権をセットします。

CloudFrontの設定

今回の動作確認用に、新しいDistributionを作成します。

Origin SettingsではOrigin Domein Nameに先ほど用意したhtmlファイルが格納されているバケットを指定します。他の設定はデフォルトのままでOKです。Create Distributionのボタンを選択します。

StatusDeployedになるまで待ちます。(ちょっと時間がかかります。)

StatusDeployedになったらアクセスしてみます。S3で公開設定されているURLのドメインを先ほど作成したDistributionDomain Nameに変更してアクセスします。ダイアログが出ることを確認します。

Lambdaの設定

新しいLambda Functionの作成

iframeの動作を無効化するための設定を行なっていきます。

はじめに、「バージニア北部」のリージョンに移動します。 Lambda@Edgeの関数の作成は現在バージニア北部リージョンにしか対応していないためです。 (参考:Lambda 関数の要件と制限 - Amazon CloudFront)

次に、新しいLambda Functionを作成します。

テンプレートを使って作成していきます。 関数の作成ページで「設計図の使用」を選択し、cloudfrontというキーワードでフィルタリングします(私は全部小文字で入力しました)。すると、「cloudfront-modify-response-header」という設計図が表示されますので、それを選択して設定ボタンを選択します。

基本的な情報の設定画面で、次のように入力して「関数の作成」ボタンを選択します

  • 名前:任意の名前を設定(例:cf-mod-response-header
  • ロール:デフォルト(AWSポリシーテンプレートから新しいロールを作成)
  • ロール名:任意の名前を設定(例:cf-mod-response-header-role
  • ポリシーテンプレート:デフォルト(「基本的なLambda@Edgeのアクセス権限(CloudFrontトリガーの場合)」が設定された状態)

正常に作成できれば「関数 cf-mod-response-header が正常に作成されました。~」というメッセージが表示され、Lambda関数の編集画面に移ります。

コードの変更

[設定]-[Designier]からLambda Functionの項目を選択してコードを編集します。 以下の内容に置き換えて「保存」します。

'use strict';
exports.handler = (event, context, callback) => {
    
    //Get contents of response
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    //Set new headers 
    headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}]; 
    
    //Return modified response
    callback(null, response);
};

こちらは公式記事のコードからx-frame-optionsに関する部分だけ流用させていただいたコードです。(チュートリアル: シンプルな Lambda@Edge 関数の作成

CloudFrontトリガーの設定

次に、[設定]-[Designer]からCloudFrontの項目を選択して設定を行います。 「Lambda@Edgeへのデプロイ」ボタンを選択します。

Lambda@Edgeへのデプロイ画面で次のように設定します。

  • Distribution:前セクションで作成したCloudFront Distributionを選択
  • キャッシュ動作:デフォルト(*
  • CloudFrontイベント:オリジンレスポンス
  • Lambda@Edgeへのデプロイを確認:チェック

今回はCloudFrontイベントにおいてオリジンレスポンス時に発火するイベントを用意することになるので、オリジンレスポンスを設定します。 (参考:Lambda 関数をトリガーできる CloudFront イベント

デプロイが正常に受け入れられると、ディストリビューションに設定した項目のStatusIn Progressになります

動作確認

StatusDeployedになったら、CloudFront経由で先のhtmlに再度アクセスしてみます。

今度はダイアログが出ないことが確認できました。

さいごに

今回触りながら調べてみて、色々と便利なことも分かりましたが、個人的に気になった制限事項もありました。

  • 同時実行数がデフォルトでは一つのAWSアカウント毎に1000まで

それを踏まえ、想定される実行数・実行速度に収まりそうかどうか(例えば、CloudFrontへのリクエストの度に毎回Lambda@Edgeの関数が呼ばれて想定外にリクエスト数が多い状況になっていたり、特定のロケーションやその他条件で起動が遅くなったりするケースが頻発したりといったことがあると思いました。)、さらに、そもそもLambda@EdgeでやろうとしていることはWEBサーバ側で十分に処理できるのではないかどうか、等よく考えた上で利用する必要があるかなと思いました。