果報

二度寝して待つ

Cloudflare Workersを使って「Security Headers」で最高評価のA+を取得する

はじめに

HTTPセキュリティヘッダを評価してくれる「Security Headers」というサイトあります。

securityheaders.com

ここにURLを入力すると、どんなサイトでも瞬時に採点してくれるのですが、最高評価のA+を取得するのはなかなか難しいようで、2024/8/9現在でサイト全体のたった2%程度しかありません。

ただ、Security Headersの中の人が書いた以下の記事を参考に、Cloudflare Workersを使えば、簡単にA+を取得できます。

scotthelme.co.uk

Cloudflare Workersとは、CloudflareのCDN上で任意のコードを実行できるサービスです。CDN上で実行されるので、サーバレスで、レスポンスが高速なのが利点です。

www.cloudflare.com

また、そもそもHTTPセキュリティヘッダとはなんぞやという疑問に対しては、以下の記事が分かりやすかったです。

www.m3tech.blog

A+の取得方法

A+の取得方法は、ざっくり書くと以下の3ステップです。

  1. Cloudflareでアカウントを作成し、自サイトを登録する
  2. Cloudflare Workersを新規作成し、このコードを貼り付ける
  3. 2で作成したWorkerを動作させる

本来は、ApacheやNginx、CaddyといったWebサーバの設定を変更する必要がありますが、この方法なら、Webサーバの設定を変更できない環境やサービスに対しても実装できます。

注意点として、Cloudflare Workersの無料枠が10万リクエスト/日のため、アクセス数の多いサイトは厳しいです。ただし、Workerの設定で「Fail open (proceed)」(日本語で「失敗オープン(続行)」)を選択すると、リクエスト上限を超えたら自動でWorkerを迂回してくれるので、勝手に課金されたりアクセス不可になることはありません。

コード説明

A+を取得する目的はクリアできたわけですが、私自身の勉強を兼ねて、このスクリプトで何が行われているのか、少し解説します。

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",
    "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の詳細は長くなるので割愛
  • Permissions-Policy: ブラウザの機能やAPIを制限する設定
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に従って削除し、その結果を新しいヘッダ情報として返却します。

解説は以上です。お疲れさまでした。

おわりに

サイトによってはスクリプトが合わず、意図したとおり動作しない可能性もあります。その場合は適宜コードを修正して、自サイトに適したセキュリティヘッダにしてください。