他サイトからの画像埋め込みを防止する

「直リン禁止」などというローカルルールはすっかり聞くことがなくなった昨今ですが、ほとんどのサイトにとって、画像ファイルなどのリソースを <img> 等で他サイトに埋め込まれると困るというのは昔も今も変わらないものと思います。

なぜ困るのかというと、巨大なリソースを他サイトから呼び出されることによるサーバー負荷の懸念という面もあるでしょうが、なんといっても第三者に「アドレスバーに元URLが表示されない方式」を採られることにより、リソースの所有権を閲覧者に誤認される恐れがあるからです。

具体的なマークアップで示すと次のようなことになります。

  • <a href="https://example.com/image.png">Image</a> ✔ これは問題ない
  • <img src="https://example.com/image.png" alt=""/> ✘ これを禁止したい(403 を返す、別の画像を表示する等)

画像を埋め込むのは <img> だけでなく、 <iframe><object>, <embed> の方法もありますが、これらも同様に禁止したいです。

結論を先に言うと、2020年8月現在、これをすべてのブラウザでその設定に関わらず完全に実現する技術は残念ながら存在しません。

  • リファラーをチェックするのが昔から知られたやり方ですが、リファラーは無効にしたり偽装したりすることができますから(ユーザーにはそうする権利があります)、対応方法を間違えると何の悪意もないユーザーが本来のサイトで画像を見られないという事態が起こりえます。

しかし「100%完全でなくても、大方のユーザーに対して制御できれば良い」のであれば、いくつか方法はあります。

リクエストヘッダー

§

Fetch Metadata Request Headers(Sec-Fetch-*

§

これまでリソースの呼び出し元が <a><img> かを判別する機能はありませんでしたが、2019年になってFetch Metadata Request Headers(w3c.github.io)が W3C の Working Draft に上がってきました。

この仕様では

  • Sec-Fetch-Dest
  • Sec-Fetch-Mode
  • Sec-Fetch-Site
  • Sec-Fetch-User

の4つのヘッダーが規定されており、これらを組み合わせることで判別が可能です。

Sec-Fetch-Dest の値が embed, image, object, iframe のいずれかであり、かつ Sec-Fetch-Sitecross-site だったなら「"cross-site" なサイトからアドレスバーに元URLが表示されない方式で画像が埋め込まれている」と判断できるでしょう。

ただし、すべてのケースで使えるわけではありません。

  • Sec-Fetch-Site はあくまで origin の関係性を示すものに過ぎず、ドメイン名による判別はできない[1]
  • HTTPS なコンテキストでのみ利用できる(HTTP の時は送出されない)
  • 2020年8月現在、Chome系のブラウザ(Chrome, Vivaldi, Edge 等)しか対応しておらず、 Firefox 80 や Safari 13.1、 IE 11 などはこれらのヘッダを送出しない。

HTTPS かつ単一ドメインで運用されているサイトであれば、対応ブラウザではこの機能を使うことで確実な判別ができます。画像(<img>)は許可するが、動画(<video>)や音声(<audio>)は許可しないといったきめ細かなルール策定もできそうです。

Accept と Referer

§

Fetch Metadata Request Headers に対応していない Firefox や Safari でも判別できる方法はないでしょうか。

呼び出され方によって値を変えるリクエストヘッダーに Accept があります。この値がどうなっているか調べてみました。

各ブラウザの詳細は上記デモページを見て頂ければと思いますが、一例として Chrome 85 ではこうなります。

URL直打ち、ないし <a href> による遷移時
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
<img>
image/avif,image/webp,image/apng,image/,/*;q=0.8
<object type="image/png">
/
<obeject type="image/svg+xml">
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
<embed type="image/png">
/
<embed type="image/svg+xml">
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9

もし、自サービスでの画像埋め込みが <img> で統一されているのならば、Accept の値に text/html が含まれているかどうかで判別が可能です。そのうえで Referer ヘッダが存在し、かつそのドメインをチェックすれば自サイトからの呼び出しかどうかが分かります。

一方、次のような注意点があります。

  • 自サービスで画像埋め込みに <object><embed> を使っている場合は判別できない
  • 本来、Accept はこういう判別に使うものではないし[2]、その値はブラウザのアップデートに伴い将来的に変わる可能性もある(実際これまでも何度も変わってきた)

また、Referer ヘッダはユーザーのブラウザ設定によって無効にされたり、実際とは異なる値に偽装されたりする可能性があります。なので、単純に「リファラーのドメインがホワイトリストに一致しているか」という判定をしてしまうと、本来のサイトであっても画像を見られないユーザーが出てきてしまいます。そのため以下のような判定にすべきでしょう。

  1. まず Referer ヘッダの有無を調べる
  2. Referer ヘッダが送出されている場合はホワイトリストとの照合を行い、他サービスであれば 403 を返す。Referer ヘッダが送出されていない場合は照合せず本来の画像を返す。

この手順のうち「Referer ヘッダが送出されていない場合は照合せず」の部分が判別が不完全にならざるを得ないところです。以前はブラウザ設定やアドオン等で意図的に無効にしているユーザーは少数派と見なして許容することもできたでしょうが、昨今は画像を勝手に埋め込んだ他サービス側がページに Referrer-Policy: no-referrer を設定することで、すべてのユーザーがリファラーを送らないようにされてしまうことも可能です。

要するに、Accept, Referer ともに不完全な判別しかできないので、仮に使うとしてもあくまでも Fetch Metadata Request Headers が普及するまでの一時しのぎという観点でやるべきでしょう。

レスポンスヘッダー

§

画像リソースのリクエストに対して、常に特定のレスポンスヘッダーを送ることで解決できるのならば、サーバー設定は一瞬で済むので話は簡単です。関係しそうなヘッダーとして X-Frame-OptionsContent-Security-Policy の2つを調べてみました。

ただし、どちらも今回の判別には使えません。せっかく調べたので結果を残しておきますが、とくに読まなくても大丈夫です。

X-Frame-Options

§

2009年〜2010年にかけて IE 8 や Firefox 3.6.9 に X-Frame-Options(tools.ietf.org) が実装されました。レスポンスヘッダーでこれを設定することで、以下の要素における埋め込み可否をコントロールすることができます。

  • iframe
  • frame
  • obeject
  • applet
  • embed

しかし、これは Web の著作物を保護する目的ではなく、クリックジャッキングを防止するセキュリティ面での意味合いから導入されたもので、基本的には HTML コンテンツの制御しかできません。

X-Frame-Options: DENY を設定したHTMLファイルと画像ファイルを <iframe>, <object>, <embed>, <img> で埋め込んだ際の各ブラウザの挙動を下表に示します。

HTML Firefox 80 Chrome 85 Safari 13.1 IE 11
<iframe> ✘ ブロック ✘ ブロック ✘ ブロック ✘ ブロック
<object type="text/html"> ✘ ブロック ✘ ブロック ✘ ブロック ✘ ブロック
<object type="image/png"> ✘ ブロック ✔ 読み込む ✔ 読み込む ✔ 読み込む
<embed type="text/html"> ✘ ブロック ✘ ブロック ✘ ブロック (未対応)
<embed type="image/png"> ✘ ブロック ✔ 読み込む ✔ 読み込む (未対応)
<img> ✔ 読み込む ✔ 読み込む ✔ 読み込む ✔ 読み込む

このように、 Firefox は <object><embed> による画像埋め込みをブロックすることができますが、他のブラウザは X-Frame-Options: DENY を設定しても読み込まれてしまいます。また、 <img> 要素に対してはどのブラウザも効果がありません(仕様で対象要素に含まれていないので当然です)。

Content-Security-Policy: frame-ancestors

§

上記で紹介した X-Frame-Options はもう時代遅れで、今の時代は Content Security Policy (CSP) の frame-ancestors ディレクティブ(W3C)を使います。IE 以外のすべてのブラウザが対応しています[3]。両方のヘッダーがある場合は CSP の指定が優先され、X-Frame-Options は無視されます[4]

Content-Security-Policy: frame-ancestors 'none' を設定したHTMLファイルと画像ファイルを <iframe>, <object>, <embed>, <img> で埋め込んだ際の各ブラウザの挙動を下表に示します。

HTML Firefox 80 Chrome 85 Safari 13.1 IE 11
<iframe> ✘ ブロック ✘ ブロック ✘ ブロック ✔ 読み込む
<object type="text/html"> ✘ ブロック ✘ ブロック ✘ ブロック ✔ 読み込む
<object type="image/png"> ✘ ブロック ✔ 読み込む ✔ 読み込む ✔ 読み込む
<embed type="text/html"> ✘ ブロック ✘ ブロック ✘ ブロック (未対応)
<embed type="image/png"> ✘ ブロック ✔ 読み込む ✔ 読み込む (未対応)
<img> ✔ 読み込む ✔ 読み込む ✔ 読み込む ✔ 読み込む

対応していない IE を除き、X-Frame-Options と同じ結果です。そのため、 CSP を画像の読み込みをブロックする用途に使うことはできません。

脚注