X-Frame-Options と CSP frame-ancestors の違い:クリックジャッキング対策ヘッダーを両方送るとどうなるか

iframe への埋め込みを防ぐ2つのヘッダーの違いを CSP Level 3 と HTML 標準で整理しました。両方送ったときの優先順位、ALLOW-FROM が無視される理由、meta タグでは効かない理由を解説します。

開発Webセキュリティ

他のサイトが自分のページを <iframe> に読み込み、その上に透明なボタンを重ねると、利用者は何を押しているのか分からないまま自分のページのボタンを押してしまいます。これがクリックジャッキングで、仕様では UI リドレッシング(UI redressing)と呼ばれます。対策は「このレスポンスを他のページの中に表示しないでほしい」とレスポンスヘッダーでブラウザーに伝えることで、そのためのヘッダーが X-Frame-Options と CSP の frame-ancestors ディレクティブです。

X-Frame-Options の値は2つだけ

X-Frame-Options は2013年に RFC 7034(Informational)としてまとめられましたが、現在の処理規則は WHATWG HTML 標準の 7.7 節にあります。HTML 標準は、この定義が RFC 7034 に取って代わると明記しています。

有効な値は DENY と SAMEORIGIN の2つです。

  • DENY:どのページの中にも表示しません。
  • SAMEORIGIN:この文書を包んでいる文書を外側へ順にたどってすべて確認し、1つでもオリジンが異なればブロックします。

RFC 7034 にあった ALLOW-FROM https://example.com について、HTML 標準は「実装しない」とはっきり書いています。この値だけを送ると不正な値として扱われ、ヘッダーがない場合とまったく同じに処理されます。この場合、X-Frame-Options による埋め込み制限はかかりません。

値が複数ある場合にも規則があります。SAMEORIGIN, DENY のように複数の値のどれかが deny、sameorigin、allowall なら埋め込みをブロックし、すべて不正な値ならヘッダーがないものとみなします。1行にカンマ区切りで書いても、複数行に分けて送っても結果は同じです。

frame-ancestors は許可先をソースリストで書く

CSP Level 3 の frame-ancestors は、frame、iframe、object、embed でこのリソースを埋め込める URL を制限します。値は 'none'、'self'、または https: のようなスキームや https://partner.example のようなホストを空白区切りで並べたソースリストです。

ブラウザーは包んでいる文書を外側へたどり、各祖先文書のオリジンを URL として解析した値がこのソースリストに一致するかを確かめます。1つでも一致しなければブロックします。一致の判定は単純なオリジン比較ではなく、スキーム・ホスト・ポートについての CSP のマッチング規則に従います。

CSP の仕様は、'none' は DENY と、'self' は SAMEORIGIN と「おおむね同じ」だと説明しています。まったく同じではなく、たとえばポートが同じか、それぞれのスキームの既定ポートであれば、'self' は同じホストの https 側にも一致します。X-Frame-Options では書けない「自サイトと特定のパートナーだけ許可」も、こちらなら書けます。

注意点が2つあります。

  • frame-ancestors は default-src にフォールバックしません。default-src 'none' だけでは frame-ancestors の制限は生じないので、このポリシーで埋め込みを制御したいなら frame-ancestors を明示する必要があります。
  • <meta http-equiv="Content-Security-Policy"> で指定した frame-ancestors は無視されます。X-Frame-Options も RFC 7034 の4節で HTTP ヘッダーとして送るものと定められ、meta タグで宣言すると無視されるとされています。

両方送った場合

現在の HTML 標準の処理アルゴリズムでは、先に CSP を見ます。適用(enforce)中のポリシーに frame-ancestors があれば、X-Frame-Options は読まれずに飛ばされます。CSP の仕様も、後方互換を保って導入できるように frame-ancestors が X-Frame-Options より優先されると書いています。

一方、Content-Security-Policy-Report-Only で送った frame-ancestors は適用中のポリシーではありません。その場合は X-Frame-Options が判定に使われます。レポート専用モードで新しいポリシーを試している間も、既存のヘッダーによる保護は続きます。

2つのヘッダーを併せて送る場合は、次のように設定できます。

Content-Security-Policy: frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN

frame-ancestors を適用するブラウザーは1行目で判定し、2行目を無視します。frame-ancestors を適用せず X-Frame-Options を参照するクライアントでは、2行目が代わりに使われます。

パートナーを許可するときは1行目に 'self' https://partner.example のようにソースを追加します。ただし X-Frame-Options にはホストを指定する有効な値がないため、2つのヘッダーで同じ許可リストを表すことはできません。2行目を SAMEORIGIN のままにすると、1行目を適用するブラウザーではパートナーからの埋め込みが許可され、X-Frame-Options で判定するクライアントではブロックされます。

2行目を ALLOW-FROM に置き換えるのはやめましょう。X-Frame-Options が参照される場合、この値は何の保護にもなりません。1行目を適用するブラウザーは引き続き frame-ancestors で判定します。

motitory.com も public/_headers で静的なレスポンスすべてにこの2つのヘッダーを設定しています。

このヘッダーがしないこと

どちらのヘッダーも「この文書を別の文書の中に表示してよいか」だけを決めます。トップレベルのウィンドウで開かれる場合は判定そのものが行われず、スクリプトや画像の読み込みなど CSP の他のディレクティブが扱う範囲にも影響しません。

ヘッダーは文書のレスポンスごとに必要です。一部のパスで抜けていないか、開発者ツールのネットワークパネルや curl -I で実際のレスポンスを確認してください。

ブログへ