他サイトからの画像埋め込みを防止する
「直リン禁止」などというローカルルールはすっかり聞くことがなくなった昨今ですが、ほとんどのサイトにとって、画像ファイルなどのリソースを <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-Site
が cross-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
があります。この値がどうなっているか調べてみました。
- Accept デモページ(
labs.w0s.jp
)
各ブラウザの詳細は上記デモページを見て頂ければと思いますが、一例として 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
ヘッダはユーザーのブラウザ設定によって無効にされたり、実際とは異なる値に偽装されたりする可能性があります。なので、単純に「リファラーのドメインがホワイトリストに一致しているか」という判定をしてしまうと、本来のサイトであっても画像を見られないユーザーが出てきてしまいます。そのため以下のような判定にすべきでしょう。
- まず
Referer
ヘッダの有無を調べる Referer
ヘッダが送出されている場合はホワイトリストとの照合を行い、他サービスであれば 403 を返す。Referer
ヘッダが送出されていない場合は照合せず本来の画像を返す。
この手順のうち「Referer
ヘッダが送出されていない場合は照合せず」の部分が判別が不完全にならざるを得ないところです。以前はブラウザ設定やアドオン等で意図的に無効にしているユーザーは少数派と見なして許容することもできたでしょうが、昨今は画像を勝手に埋め込んだ他サービス側がページに Referrer-Policy: no-referrer
を設定することで、すべてのユーザーがリファラーを送らないようにされてしまうことも可能です。
要するに、Accept
, Referer
ともに不完全な判別しかできないので、仮に使うとしてもあくまでも Fetch Metadata Request Headers が普及するまでの一時しのぎという観点でやるべきでしょう。
レスポンスヘッダー
画像リソースのリクエストに対して、常に特定のレスポンスヘッダーを送ることで解決できるのならば、サーバー設定は一瞬で済むので話は簡単です。関係しそうなヘッダーとして X-Frame-Options
と Content-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>
で埋め込んだ際の各ブラウザの挙動を下表に示します。
- X-Frame-Options デモページ(
labs.w0s.jp
)
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
ディレクティブを使います。IE 以外のすべてのブラウザが対応しています[3]。両方のヘッダーがある場合は CSP の指定が優先され、X-Frame-Options
は無視されます[4]。
Content-Security-Policy: frame-ancestors 'none'
を設定したHTMLファイルと画像ファイルを <iframe>
, <object>
, <embed>
, <img>
で埋め込んだ際の各ブラウザの挙動を下表に示します。
- CSP: frame-ancestors デモページ(
labs.w0s.jp
)
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 を画像の読み込みをブロックする用途に使うことはできません。
脚注
-
1.
自サービスが複数のドメインで運用されている場合のドメインの切り分けはできません。当該サービスが hogehoge.example.com で運用されているとして、ドメインは異なるが自社が管理する hogehoge-cdn.example.net からの呼び出しは許可するが、他社管理の piyopiyo.example.org からの呼び出しは禁止するといった判定はできません。 ↩ 戻る
-
2.
RFC 7231 で
The "Accept" header field can be used by user agents to specify response media types that are acceptable.
(tools.ietf.org
)と記されているように、ユーザーエージェントが受け入れ可能なメディアタイプを指定するために利用されるものです。 ↩ 戻る -
3.
「Can I use...」の headers HTTP header: csp: Content-Security-Policy: frame-ancestorsより。 ↩ 戻る
-
4.
CSP 仕様の 7.7.1. Relation to X-Frame-Optionsより。 ↩ 戻る