「Security Headers」で簡単に最高評価のA+を取得する方法

タイトルは釣り、と言いたいところだけど、実はあながち嘘ではない。本当に、誰でもA+を取ることができる。そう、Cloudflareさえあれば…。

というわけで今回は「Security Headers」というHTTPセキュリティヘッダを評価するサイトで、さくっと最高評価のA+を取得するための方法を紹介したいと思う。

A+の証拠画像

まあこのサイトのURLを入力してもらえば誰でも確認できるんだけど、一応A+の証跡を残しておく。

ちなみに、2019年12月時点での評価分布をみると

なので、A+の当サイトは上位1.6%(949,707/59,042,241)となる。わはは、すごいだろう!(棒)

A+の取得方法

勿体ぶらずに早速手順を書くと

  1. Cloudflareでアカウントを作成し、自サイトを登録する(既にあれば不要)
  2. Cloudflare Workersを新規作成し、ここで紹介されているコードを貼り付ける
  3. 2で作成したWorkerを動作させる

おわり。まさかの3ステップ。

正攻法では、ApacheやらNginxのconfをいじったりHTMLをいじったり、色々と面倒なチューニングをする必要があるし、そもそもWebサーバの設定を変更できる環境でないと対応できなかったりする。

でもこの方法なら、誰でも、たったの3ステップで、しかも確実にA+を取れる。すごくない?もうみんなこの方法でいいと思う(暴論)

補足事項をいくつか

Cloudflare Workersの詳細については以下の記事が詳しい。端的にいうと「CDN上で任意のコードを実行できるサービス」といえば伝わるだろうか。

Cloudflare Workersのチュートリアルをやってみた | DevelopersIO
はじめに CX事業本部@札幌の佐藤です。 10/21(月) 〜 10/22(火)で開催された Serverless Days Tokyo 2019に参加してきました。そこで、Cloudflare WorkersというCl …

補足というか注意事項として、Cloudflare Workersの無料枠が10万リクエスト/日なので、アクセス数が多いサイトは厳しいかもしれない。ちなみにうちの場合、1日100PVくらい(管理者含まず)でだいたい3,000~5,000リクエストだったので、あと10倍増えても余裕なはず。

また、Workerの設定で「Fail open (proceed)」を選択しておくと、リクエスト上限を超えたら自動でWorkerを迂回してくれるので、勝手に課金されたりアクセス不可になったりしない。

一応参考までに、主要な設定画面を貼っておくので参考にしてもらえればと思う。

さらに、常にWorkerを迂回させたいページがあるときは、Workerの適用条件を追加する。

自分の環境では、Workerを適用した状態でWordPressを更新すると、更新過程(更新しています…的なやつ)が表示されなくなった。あくまで表示されないだけで更新自体は正常に行われるけど、気になる人は以下のように管理ページをWorker適用除外にすると良い。

単にA+評価取るだけならここで終了。ここから先は少し技術的な話。

なぜA+を取得できるのか

この手法を紹介しているのは以下のサイト。どうやら「Security Headers」の中の人らしい。そりゃどうりで確実にA+取れるわけだよ。

The brand new Security Headers Cloudflare Worker
For a long time it's been difficult to set security headers when you use certain hosted solutions like Ghost Pro or GitHub Pages. All of that is about to change...

記事の中で丁寧にコードの解説をしてくれているけど、私のように英語が苦手でセキュリティヘッダの知識がない人向けに、ここでざっくり説明する。

コード説明

let securityHeaders = {
        "Content-Security-Policy" : "upgrade-insecure-requests",
        "Strict-Transport-Security" : "max-age=1000",
        "X-Xss-Protection" : "1; mode=block",
        "X-Frame-Options" : "DENY",
        "X-Content-Type-Options" : "nosniff",
        "Referrer-Policy" : "strict-origin-when-cross-origin",
        "Feature-Policy" : "camera 'none'; geolocation 'none'; microphone 'none'",
        "Permissions-Policy" : "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
}

サイトからのレスポンス時に追加するセキュリティヘッダのリスト。それぞれの意味は以下のとおり。

  • Content-Security-Policy
    XSSなどの影響を軽減するための設定。”upgrade-insecure-requests”はhttpのURLをhttpsに置き換えるオプション
  • Strict-Transport-Security
    HTTP の代わりに HTTPS で通信するよう指示する。”max-age”はHTTPSでの通信時間(単位:秒)
  • X-Xss-Protection
    XSSフィルタ設定。”1; mode=block”はフィルタを有効化し、XSS検知時にレンダリングを停止する
  • X-Frame-Options
    ページのフレーム内表示を許可するか。”deny”はいかなるページも許可しない最も強力な設定
  • X-Content-Type-Options
    Content-Typeで指定したファイル形式を強制させる設定。主にXSS対策
  • Referrer-Policy
    ページから遷移する際のリファラー情報を制御する設定。”strict-origin-when-cross-origin”の詳細は長くなるので割愛
  • Feature-Policy
    ブラウザの機能やAPIを制限する設定。ここではカメラと位置情報とマイクを禁止している
【2020/12/6追記】久々に確認したところ、どうやらFeature-PolicyがPermissions-Policyに改名され、パラメータの指定方法も変更されたらしく、上記コードではA判定になってしまう様子。なので、Feature-Policyの行を変更した。
let sanitiseHeaders = {
        "Server" : "My New Server Header!!!",
}

オリジンサーバが使用するソフトウェアの情報が格納されるServerヘッダに「My New Server Header!!!」を上書きして、元の情報を秘匿する定義。

let removeHeaders = [
        "Public-Key-Pins",
        "X-Powered-By",
        "X-AspNet-Version",
]

セキュリティの観点から削除するヘッダのリスト。バージョン情報が漏洩するリスクなどを防ぐ目的らしい。

addEventListener('fetch', event => {
        event.respondWith(addHeaders(event.request))
})

サイトへのリクエスト時にaddHeaders関数を呼び出し、その結果をレスポンスする設定。

async function addHeaders(req) {
        let response = await fetch(req)
        let newHdrs = new Headers(response.headers)

        if (newHdrs.has("Content-Type") && !newHdrs.get("Content-Type").includes("text/html")) {
                return new Response(response.body , {
                        status: response.status,
                        statusText: response.statusText,
                        headers: newHdrs
                })
        }

        let setHeaders = Object.assign({}, securityHeaders, sanitiseHeaders)

        Object.keys(setHeaders).forEach(name => {
                newHdrs.set(name, setHeaders[name]);
        })

        removeHeaders.forEach(name => {
                newHdrs.delete(name)
        })

        return new Response(response.body , {
                status: response.status,
                statusText: response.statusText,
                headers: newHdrs
        })
}

そしてこれがaddHeaders関数。これまでに定義したセキュリティヘッダを元のヘッダに上書きまたは追加する。また、不要なヘッダはremoveHeadersに従って削除し、その結果を新しいヘッダ情報として返却する。

以上、お疲れさまでした。

おわりに

実装自体は1時間もかからなかったけど、勉強や翻訳にその3倍くらいかかった。

なお、サイトによっては今回紹介した定義が合わず、表示や動作が意図したとおり行われなくなる可能性もある。その場合は適宜コードを修正して、自サイトに適したセキュリティヘッダを実現してほしい。

タイトルとURLをコピーしました