静的サイトでもHTTPセキュリティヘッダーは要る、というのが私の立場です。「静的サイトだから不要」という判断はWebサイトの診断でよく見かけますが、これは守る対象を取り違えています。
HTTPヘッダーが守るのは、アプリケーションのロジックではなくブラウザの解釈です。だからSSGかSSRかは関係ありません。<iframe> に自サイトを埋め込まれるclickjacking、Content-Typeが曖昧なファイルをブラウザがJavaScriptとして実行してしまう挙動、外部サイトへの遷移時にRefererヘッダーで内部URLが漏れること、HTTPとHTTPSが混在したときのダウングレード攻撃。これらはアプリケーションコードでは塞げず、ヘッダーでしか塞げません。
そしてCloudflare Pagesなら、サーバー側(Express、Astroミドルウェア、Edge function)を持ち出さなくても public/_headers ファイル1つで設定できます。以下、最低限入れるヘッダーと、CSPをReport-Onlyから段階導入する手順をまとめます。
最低限入れるヘッダー
HSTS、X-Content-Type-Options、Referrer-Policy、Permissions-Policy、X-Frame-Options。これらを先に入れます。ほとんどはコピペで終わりますが、HSTSだけは事故りやすいので後で詳しく書きます。
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
ブラウザに、このドメインは今後2年間は必ずHTTPSで接続しろ、と伝えます。ここで気をつけたいのが includeSubDomains で、これを付けるとサブドメインもまとめてHTTPS強制になります。社内ツールやレガシーな管理画面がHTTP-onlyでサブドメインに残っていると、その瞬間にアクセスできなくなります。企業サイトでは入れる前にサブドメインを棚卸ししてください。ここを確認せずに入れて自分の首を絞めるのが、このヘッダーの典型的な事故です。preload はHSTS Preload Listへの登録シグナルで、登録自体は別途 hstspreload.org で申請します。
X-Content-Type-Options: nosniff
X-Content-Type-Options: nosniff
ブラウザのMIME type推測を切って、Content-Typeで宣言したタイプをそのまま信じさせます。これがないと、CSSや画像として配信したファイルが状況によってJavaScriptとして実行されることがあります。値はこの1行だけで、迷う余地はありません。
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: strict-origin-when-cross-origin
クロスオリジン遷移ではorigin(https://example.com)だけを送り、パスやクエリは送りません。同origin内なら完全なURLを送ります。最近のブラウザはこれをデフォルトにしているので、明示しておけば十分です。
Permissions-Policy
Permissions-Policy: camera=(), microphone=(), geolocation=()
ブラウザの機能を明示的にdeny。() (空のorigin一覧)は「どこからも使えない」の意。コーポレートサイトにcamera、mic、geolocationはまず不要です。
かつてFLoCのopt-outとして interest-cohort=() を加える慣行がありましたが、FLoC自体が終了してTopics APIに移行したため、現在この指定は実質無意味です。古い記事や雛形からコピーすると紛れ込みやすいので、見つけたら外して問題ありません(残っていても害はない)。
X-Frame-Options: DENY
X-Frame-Options: DENY
自サイトを <iframe> 内に埋め込み禁止(clickjacking抑止)。CSPの frame-ancestors 'none' でも同等の制御ができますが、古いブラウザ対応としてX-Frame-Optionsも併設します。
_headersに書く
Cloudflare PagesはNetlifyと同じ _headers ファイル形式をサポートします。public/_headers に書けば、ビルド時に dist/_headers に複製され、配信時にこのルールが適用されます。
/*
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
X-Frame-Options: DENY
/* は全パスに適用。特定パスだけoverrideしたい場合は /admin/* のようなパターンで別ルールを書きます。デプロイ後は curl -I https://example.com/ でヘッダーが返るか確認。
CSPはReport-Onlyから段階導入する
Content-Security-Policy(CSP)は、ここまでのヘッダーより踏み込んだ制御ができる分、設定ミスでページの一部が動かなくなる事故も起こしやすいです。最近のビルドツールはインラインスクリプトやインラインスタイルを必要に応じて生成するので、'self' だけに絞ると正規のリソースまで拒否されることがあります。
そこで Content-Security-Policy-Report-Only で違反レポートをログ収集してからenforce切替する2段階構成を組みます。
Phase A: Report-Onlyで検証
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.web3forms.com; form-action 'self' https://api.web3forms.com; frame-ancestors 'none'; base-uri 'self'; report-uri https://example.report-uri.com/r/d/csp/reportOnly; report-to default;
Reporting-Endpoints: default="https://example.report-uri.com/r/d/csp/reportOnly"
レポート送信先は、新規構築なら Reporting-Endpoints (現行のレポーティングAPI)を使い、互換のためにCSPに report-uri も併記しておく構成が無難です。MDN上では Report-To ヘッダーはdeprecated扱い。
Report-Onlyはブロックせずレポートだけ送るモード。1〜2週間運用して違反0件を確認します。
Phase B: enforce切替
違反が0件になったら、ヘッダー名を Content-Security-Policy (Report-Onlyではない)に変えてenforce開始。
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; ...
CSPディレクティブの例
script-src 'self' で自サイトのJavaScriptのみ、インライン不可。style-src 'self' 'unsafe-inline' で <style> 直書きや要素の style= 属性が出る場合に 'unsafe-inline' を許容(TailwindのようなAtomic CSSフレームワーク自体は外部CSSとして生成されるので、'unsafe-inline' が必要かはAstroやビルド成果物に何が含まれるか次第)。connect-src 'self' https://api.web3forms.com でXHRやfetchの宛先を制限。form-action 'self' https://api.web3forms.com でform POST先を制限。frame-ancestors 'none' でiframe埋め込み禁止。base-uri 'self' で <base> タグのoriginを制限。
HSTS preload登録は本番運用が安定してから
Strict-Transport-Security に preload ディレクティブを入れただけでは、HSTS Preload Listに自動登録されません。 hstspreload.orgで申請する別ステップが要ります。
申請の前提条件: max-age >= 31536000 (1年以上)。includeSubDomains あり。preload ディレクティブあり。HTTPSのみで配信、HTTPからHTTPSのリダイレクト設定済み。すべてのサブドメインがHTTPSに対応。
preload list登録後の解除は数ヶ月単位かかります。サブドメインでHTTP-onlyのサービスを残している間はpreloadしない。本番運用が安定してから登録します。
まとめると、HSTS、nosniff、Referrer-Policy、Permissions-Policy、X-Frame-Optionsを _headers に入れ、CSPはReport-Onlyで様子を見てからenforceに切り替えます。これでサーバーフレームワークを介さず、アプリケーションの外側に防御層を置けます。HSTSのサブドメイン強制とCSPのenforce切替だけ事故りやすいので、そこさえ慎重にやれば残りはほぼコピペで済みます。
セキュリティヘッダーの導入設計やCSPの段階移行で詰まったら、お問い合わせからどうぞ。