HTTP セキュリティヘッダとは — ブラウザに「守り方」を命じる第二の防壁 のサムネイル

HTTP セキュリティヘッダとは — ブラウザに「守り方」を命じる第二の防壁

⏱ 約 20 分 view 72 like 0 LOG_DATE:2026-05-22
目次 / TOC

HTTP セキュリティヘッダは、サーバが返すレスポンスに添える「ブラウザへの命令書」。「このサイトでは TLS を必須にしろ」「このスクリプト以外は実行するな」「iframe に埋め込ませるな」といった指示を、アプリケーションコードを一切触らずに加えられる。Same-Origin Policy (SOP) は クロスオリジンへの直接アクセスを止めるが、同一オリジン内に注入されたコード (XSS) や、自オリジンを iframe に押し込まれたとき (クリックジャッキング) には無力 — そこを埋めるのがこれらのヘッダだ。本稿は HSTS → CSP → クリックジャッキング → nosniff/Referrer → Permissions-Policy → クロスオリジン分離 → 検証と運用の順で、各ヘッダの「何を守るか」「どう壊れるか」を一本の流れで読み解く。

▸ Web 初学者へ — まずこの 3 つだけ

細部が多くて圧倒されがちだが、本質は次の 3 つだけ持ち帰ればいい。(1) セキュリティヘッダは 「ブラウザ側で動かす防御」。サーバから「こう振る舞え」と命令する。(2) アプリのコードを直さずに nginx の設定 1 行で追加できる。導入コストが軽い割に効果が大きい。(3) 一番重要なのは HSTS (TLS 強制) と CSP (スクリプト制御)。それ以外は補助。— ここを軸に各章を読めば、迷子にならない。

01

なぜブラウザに「ヘッダで命令」するのか #

ブラウザは古い HTML / 古いプロトコルにも互換性を保つように作られている。たとえばデフォルトでは HTTP のリンクをクリックすれば暗号化なしで通信するし、<script src="..."> で取ってきた JavaScript は実行するし、<iframe> で他サイトに埋め込まれるのも許す。これらは Web の機能としては正しいが、攻撃者から見ればそのまま使える攻撃面でもある。

セキュリティヘッダは、サーバ運営者が 「うちのサイトでは古い挙動を切ってくれ」とブラウザに通告する仕組み。サーバが守るのではなく、ブラウザに守らせる。だから対象が「ブラウザのユーザ」である Web アプリにしか効かず、curl や独自スクリプトは無視する。逆に言えば、ブラウザ経由で来る攻撃 (XSS / クリックジャッキング / MIME sniffing 等) に絞って絶大な効果がある

▸ かみ砕いて言うと — 「ブラウザに渡す利用規約」

普通のはがきの裏に 「未開封のまま転送禁止」「コピー禁止」と書いてあっても、配達員が読まなければ意味がない。HTTP セキュリティヘッダは ブラウザがちゃんと読んで従う「規約」。サーバが「TLS なしでは絶対繋ぐな」「このスクリプト以外実行するな」と指示し、現代のブラウザはこれに かなり厳密に従う。だから「サーバ側で何もしていない」アプリでも、ヘッダ 5 行追加するだけで防御層が一気に増える。

ヘッダで守れる攻撃を整理すると次のようになる。

攻撃 対応ヘッダ 何を止めるか
HTTP downgrade / SSL stripping Strict-Transport-Security プレーン HTTP への接続そのもの
XSS (反射型 / 格納型 / DOM) Content-Security-Policy インラインスクリプトと外部スクリプトの実行
クリックジャッキング Content-Security-Policy: frame-ancestors / X-Frame-Options 他オリジンの iframe 埋め込み
MIME sniffing による偽装ファイル実行 X-Content-Type-Options: nosniff image/jpeg 装った JS の実行
Referer 漏洩 Referrer-Policy URL に含まれるトークン・パスの流出
過剰権限 (カメラ・マイク・支払い) Permissions-Policy サードパーティが勝手に API を呼ぶこと
Spectre / Meltdown 系 Cross-Origin-{Opener,Embedder,Resource}-Policy クロスオリジンの共有メモリ・ポインタ漏洩

これらは互いに直交していて、1 つでは不十分・全部入れて初めて完成する多層防御の構成要素になる。

02

HSTS — TLS を絶対条件にする #

HSTS (HTTP Strict Transport Security, RFC 6797) は、「このサイトには HTTP では絶対に接続するな」とブラウザに記憶させるヘッダ。発行されたブラウザは、ユーザが http://example.com/login と打っても、URL バーで http:// のリンクをクリックしても、サーバに問い合わせる前に https:// に書き換えて接続する。

▸ かみ砕いて言うと — 「もう HTTP には戻らない宣言」

HTTPS でアクセスしたサイトが「次回からも必ず HTTPS で来てね」とブラウザに付箋を貼るのが HSTS。次回以降、ユーザがうっかり `http://` と打っても、攻撃者が `Location: http://...` で誘導しても、ブラウザが先回りして HTTPS に書き換える。つまり downgrade 攻撃 (SSL stripping) を仕組みごと封じる。一度受け取った付箋は max-age の期間 (通常 1〜2 年) 保持され、その間は HTTP では絶対に出ない。

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
ディレクティブ 意味
max-age=63072000 このルールを覚えておく秒数。例は 2 年 (推奨: 最低 1 年)
includeSubDomains *.example.com も全部対象にする
preload ブラウザにバンドルされた preload リストへの登録を希望する宣言

preload はオプトイン式で、hstspreload.org に登録すると Chrome / Firefox / Safari にハードコードされ、そのサイトを一度も訪れていない新規ユーザにも HSTS が効く。逆に「一度登録すると外すのが非常に重い (リスト反映に数か月)」ため、本番で確実に HTTPS のみで運用する自信が出てから登録する。

注意 — HSTS の罠

(1) 一度発行した HSTS は max-age を 0 にした応答を新たに HTTPS で返さない限り消えない。証明書を失効したサイトに HSTS を効かせていると、ブラウザは「HTTPS でしか繋がない」のに「HTTPS は証明書エラー」になり、ユーザがアクセスする手段が消える(2) `preload` 登録後は サブドメインも全部 HTTPS 化必須。社内ツールが HTTP で動いていた、というケースで一斉に死ぬ。導入は 短い max-age (例: 5 分) → 1 日 → 1 週間 → 1 年 → preload と段階的に上げる。

HPKP (Public-Key-Pins) という「証明書の公開鍵ピンニング」ヘッダも 2012 年頃に登場したが、誤設定でサイトが恒久的にアクセス不能になる事例が頻発し、Chrome 72 (2019) で削除済み。現在の標準は「証明書透明性 (CT) ログ + CAA レコード」で、ヘッダによる pinning は推奨されない。

03

CSP — スクリプト実行の許可リスト #

Content-Security-Policy (CSP)セキュリティヘッダの中で最も強力かつ最も難しい。XSS や inline script 注入による攻撃を、「許可したものだけ実行する」というホワイトリスト方式で塞ぐ。攻撃者が <script> を注入できても、CSP が許可していないスクリプトはブラウザが実行を拒否する。

▸ かみ砕いて言うと — 「実行許可リスト」

XSS の本質は サイトと同じオリジンで攻撃者のスクリプトが動いてしまうこと。CSP はサーバが 「うちで動かしていいのはこの URL のスクリプトと、この nonce 付きのものだけ」とブラウザに通告する仕組み。注入されたインライン `<script>alert(1)</script>` は nonce が無いので実行されずに静かに弾かれる。XSS の出力エスケープを忘れた箇所が 1 つ残っていても、CSP が二重の壁として効く。

代表的なディレクティブを並べる。

ディレクティブ 制御対象
default-src 他で個別指定がないリソース全般 'self'
script-src JavaScript の取得元 'self' 'nonce-abc123'
style-src CSS の取得元 'self' 'unsafe-inline'
img-src 画像 'self' data: https:
connect-src fetch / WebSocket / EventSource の接続先 'self' https://api.example.com
font-src フォント 'self' https://fonts.gstatic.com
frame-src / child-src iframe で読み込めるオリジン 'self'
frame-ancestors 自分が iframe に埋め込まれていいオリジン 'self'
form-action <form> の送信先 'self'
base-uri <base> タグで設定可能な URL 'self'
object-src <object> / <embed> (Flash 等) 'none'
report-to / report-uri 違反レポートの送信先 https://example.com/csp-report

最低限の現代的ベースラインはこうなる。

最小構成の CSP
# 自オリジンの static アセットだけ許可、object はゼロ、frame-ancestors も自分だけ Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{毎リクエストランダム}'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; report-uri /csp-report

ポイントは nonce ベースにすること。'unsafe-inline' を使うと CSP の防御力が一気に落ちる ― インライン <script>...</script> を全部許してしまうので、XSS が刺さった瞬間に CSP は無いに等しくなる。代わりにサーバ側で毎リクエスト乱数 (nonce) を生成し、自分が出力したスクリプトタグだけに <script nonce="abc123"> を付けて識別する。攻撃者には正しい nonce が分からないので注入できない。

注意 — `'unsafe-inline'` と `'unsafe-eval'`

`'unsafe-inline'` はインライン script / インライン style を全許可、`'unsafe-eval'`eval() / new Function() を許可するディレクティブ。どちらも CSP の 防御力をほぼゼロにする。Google Tag Manager 等の第三者タグが要求してくるケースが多いが、可能なら nonce / hash に移行する。両方付けた CSP は「ポリシーがあるだけで実質ノーガード」なので、Mozilla Observatory も A 評価を出さない。

段階導入には Content-Security-Policy-Report-Only を使う。同じ書式だが「ブロックせずに違反だけ報告する」モード。本番に出してアクセスログ的に違反を集め、'unsafe-inline' を外したときに何が壊れるかを把握してから本ポリシーに切り替える。新規導入時は必ず report-only から。

Trusted Types (require-trusted-types-for 'script') は CSP の上位機能で、element.innerHTML = userInput のようなそもそも危険な代入を型レベルで禁止する。Chromium 系のみだが、DOM Based XSS に対する究極の対策として大規模サイト (Gmail / Google 検索) で採用が進んでいる。

04

クリックジャッキング — X-Frame-Options と frame-ancestors #

クリックジャッキングは、攻撃者サイトに被害者サイトを <iframe> で透明に重ね、ユーザのクリックを別の場所のボタンに「乗っ取る」攻撃。被害者は「いいね」ボタンを押したつもりが、裏でログアウトや送金ボタンを押している、というシナリオ。

▸ かみ砕いて言うと — 「他人のサイトの上に透明な皮膜」

攻撃者は「クリックすると当選」ボタンを置いた偽サイトを用意し、その上に opacity:0 の iframe で銀行サイトの「送金実行」ボタンを ピクセル単位でぴったり重ねる。ユーザが「当選」を押すと、実際には iframe 越しに銀行の送金ボタンを押したことになる。iframe に埋め込めないようにすればこの攻撃は成立しないので、ヘッダで「iframe 埋め込み禁止」を宣言する。

埋め込みを止める方法は 2 系統ある。

X-Frame-Options: DENY              # どこからも iframe 不可
X-Frame-Options: SAMEORIGIN        # 自オリジンの iframe だけ可
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://partner.example.com

X-Frame-Options は古いヘッダで、DENY / SAMEORIGIN の 2 値しか取れない。CSP の frame-ancestors はその上位互換で、複数オリジンを許可リスト形式で書ける。両方付けてもよいが、frame-ancestors が指定されていれば現代のブラウザは X-Frame-Options を無視する。古いブラウザ互換のために両方入れる運用が安全。

用途 推奨設定
一切埋め込ませない (銀行・管理画面) X-Frame-Options: DENY + frame-ancestors 'none'
自社サイト内でのみ埋め込む X-Frame-Options: SAMEORIGIN + frame-ancestors 'self'
特定パートナーだけに許可 frame-ancestors 'self' https://partner.example.com (XFO は付けないか SAMEORIGIN)

ALLOW-FROM という第三の値もあったが、Chromium / Firefox いずれも実装せず事実上消滅。「特定オリジンだけに iframe を許す」用途は frame-ancestors を使うしかない。

05

MIME sniffing と Referrer の制御 #

ここから先は地味だが必須のヘッダ群。攻撃の入口を小さく削っていく類のものなので、目立たないが入れない理由がない。

X-Content-Type-Options: nosniff #

ブラウザは古い慣習で、サーバが返した Content-Type が「明らかに違うんじゃ?」と判断した場合に勝手にバイナリを覗いて MIME を推測する (MIME sniffing)。たとえば image/jpeg だが中身が JavaScript で始まっている、というファイルがあると、文脈次第で JS として実行してしまう。これは攻撃者にとっておいしい — 画像アップロード機能のあるサイトに JS を画像として上げ、<script src> で読み込ませる形の攻撃が成立する。

X-Content-Type-Options: nosniff

このヘッダがあれば、ブラウザは sniffing を完全に止め、Content-Type を額面通りに信じる。セットしない理由は存在しない

Referrer-Policy #

<a href> クリックで遷移したとき、遷移先には Referer ヘッダでどの URL から来たかが送られる。URL にトークンが入っていたり、社内ツールのパスがバレるのを防ぐにはこのヘッダで漏洩量を制御する。

Referrer-Policy: strict-origin-when-cross-origin
何が送られるか
no-referrer 一切送らない
same-origin 同一オリジンへのみフル URL を送る
strict-origin クロスオリジンにはオリジン (https://example.com) のみ
strict-origin-when-cross-origin 同一オリジンはフル URL、クロスオリジンはオリジンのみ (現代の主要ブラウザのデフォルト)
unsafe-url 常にフル URL を送る (非推奨)

現代ブラウザはこのヘッダ未指定なら strict-origin-when-cross-origin をデフォルトとして使うので未指定でもそこそこ安全だが、明示する方が将来の挙動変化に強い。URL にセッショントークンや 1 回限りリンクが乗っているサービスは no-referrer 寄りに振る。

06

Permissions-Policy — 強い API の絞り込み #

カメラ・マイク・位置情報・支払い・センサーなど、強い権限を持つ Web API を、自オリジンや特定パートナーだけに許可するヘッダ。前身は Feature-Policy で、構文を整理して 2020 年に改名された。

Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://pay.example.com")
機能 用途
camera () で全禁止、(self) で自オリジンのみ サードパーティタグが勝手にカメラを起動しない
microphone 同上 同上
geolocation (self) 広告 SDK が位置情報を抜くのを防止
payment (self "https://pay.example.com") 決済 API を自社と決済代行ドメインに限定
usb / bluetooth / serial () WebUSB 等の機器アクセスを完全禁止
interest-cohort () Google の FLoC を無効化する宣言 (歴史的)

これは「自社が使うつもりのない強い API はそもそも誰にも使わせない」という保守的設定をデフォルトにする発想。3rd party の広告タグや解析タグが navigator.mediaDevices.getUserMedia() を呼んできても、Permissions-Policy が camera=() ならブラウザは即座に拒否する。

07

クロスオリジン分離 — Spectre 後の世界 (COOP / COEP / CORP) #

2018 年の Spectre / Meltdown 脆弱性以降、ブラウザは「同一プロセス内の他オリジンのメモリも信用しない」前提に書き換えられた。SharedArrayBuffer のような高精度タイマーや共有メモリは、ブラウザがオリジンを別プロセスに隔離 (cross-origin isolated) できたときだけ使える、という制約が加わった。これを宣言するのが COOP / COEP / CORP の 3 兄弟。

ヘッダ 意味 代表値
COOP (Cross-Origin-Opener-Policy) window.opener でクロスオリジン窓を覗かれない same-origin
COEP (Cross-Origin-Embedder-Policy) 自ページに埋め込むサブリソースに CORP / CORS 同意を強制 require-corp
CORP (Cross-Origin-Resource-Policy) サブリソース側が「誰に埋め込まれていいか」を宣言 same-origin / same-site / cross-origin
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Resource-Policy: same-origin
▸ かみ砕いて言うと — 「同じ部屋に入れる相手を絞る」

古いブラウザは、複数のオリジンを 1 つのプロセスに同居させて動かしていた (メモリ節約のため)。Spectre はその同居メモリを覗き見る攻撃。対策は 「うちのオリジンを別の部屋 (プロセス) に隔離してくれ」とブラウザに頼むこと。COOP / COEP / CORP の 3 つを揃えると crossOriginIsolated === true になり、ブラウザは そのページを別プロセスで動かし、SharedArrayBuffer や performance.now() の高精度タイマーが解禁される。

実用上、SharedArrayBuffer を使わないサイトでも COOP: same-origin だけは入れておく価値があるwindow.opener 経由のクロスオリジン制御 ("tabnabbing") を遮断できるので、target="_blank" 多用サイトでは効く。COEP は外部 CDN を読みやすくしているサイトでは全部 require-corp 対応に直す必要があり、導入コストが大きい。OAuth ポップアップが必要なら COOP は same-origin-allow-popups にしてポップアップ通信は許す、という妥協値もある。

08

検証と運用 — 何を入れて、何が効いているか #

設定したつもりで効いていないヘッダは、攻撃のとき「あると思ってた防壁が無い」という最悪の結果を生む。導入したら必ず外部から検証する。

curl で生のヘッダを見る #

curl -sI でレスポンスヘッダだけ取得
# -s: 進捗を抑制 / -I: HEAD リクエスト $ curl -sI https://example.com/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer|permissions|cross-origin' strict-transport-security: max-age=63072000; includeSubDomains; preload content-security-policy: default-src 'self'; script-src 'self' 'nonce-...'; ... x-frame-options: DENY x-content-type-options: nosniff referrer-policy: strict-origin-when-cross-origin permissions-policy: camera=(), microphone=(), geolocation=(self) cross-origin-opener-policy: same-origin

外部スキャナで採点する #

securityheaders.com (Scott Helme 運営) は無料で A+ 〜 F 評価を出す軽量チェッカー。Mozilla Observatory (observatory.mozilla.org) はもう少し深く、TLS 設定や Cookie 属性まで含めた総合採点を出す。両者で A 以上が目安。

nginx での実装例 #

nginx.conf — server ブロック内
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always; add_header Cross-Origin-Opener-Policy "same-origin" always; # CSP は nonce が要るのでアプリ側で組み立てて返す方が現実的

末尾の always を忘れるとエラーページ (4xx / 5xx) でヘッダが付かない。攻撃時に出るのは大抵エラー応答なので、必ず always を付ける。

Laravel での実装例 #

ミドルウェアでまとめて付ける。nonce を毎リクエスト生成して CSP に埋める。

app/Http/Middleware/SecurityHeaders.php
// 各レスポンスにセキュリティヘッダを付与 public function handle($request, Closure $next) { $nonce = base64_encode(random_bytes(16)); view()->share('cspNonce', $nonce); // blade で {{ $cspNonce }} 参照

$response = $next($request); $response->headers->set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload'); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; " . "style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; " . "object-src 'none'; base-uri 'self'; frame-ancestors 'self'"); return $response; }

導入順序の現実解 #

1. 軽い 4 つから入れる
X-Content-Type-Options / X-Frame-Options / Referrer-Policy / HSTS (短い max-age から)。アプリの挙動を変えずに防御層が増える。
2. CSP を Report-Only で投入
Content-Security-Policy-Report-Only で違反だけ集める。インラインスクリプト / 外部スクリプトの依存をすべて洗い出し、`'unsafe-inline'` を外す道筋を作る。
3. CSP を enforce に切替
Content-Security-Policy 本ヘッダに昇格。nonce ベースに移行。Report-Only も並列で残し、新ポリシーの違反を継続収集する。
4. HSTS preload を申請
max-age=31536000; includeSubDomains; preload を 1 年安定稼働させた後、`hstspreload.org` に登録。「ハードコード組」になる。
5. Permissions-Policy / COOP-COEP-CORP
機能依存とサードパーティ埋め込みを精査して順次追加。最後にクロスオリジン分離 (SharedArrayBuffer 必要時) を狙うなら CORP 対応のリソースだけに整理する。
最後に — ヘッダだけで XSS は防げない

CSP は 「XSS が起きてしまった後に被害を抑える」防御。出力エスケープが正しければ CSP は不要、ではなく、出力エスケープが 1 か所漏れたときの保険として CSP を入れる。逆に CSP を入れたから出力エスケープを怠っていい、という発想は本末転倒。アプリ層の正しさ → ブラウザ層のヘッダ防御の順番でしか、本当の安全は来ない。