img タグからのリクエストは Origin ヘッダーを持たないのでレスポンスの Access-Control-Allow-Origin を満たしていなくても CORS 違反にならないらしいという話
歴史的経緯により img タグからのリクエストは(crossorigin 属性を付与しない限り)クロスオリジンの場合でも preflight リクエストは飛ばない。これを simple request と呼ぶことは知っていた。
しかし、画像のレスポンスヘッダーに例えばAccess-Control-Allow-Origin: https://hoge.com
が含まれる場合に、hoge.com
以外から img タグを利用してリクエストを送ると CORS 違反になるんじゃないかと思っていた。
しかし実際に試してみたら、そのような画像に対して、img タグからリクエストを発行すると正常に画像を取得することができることが分かった。
例えば、localhost で以下のコードを動かすと画像が正常に表示される。
はてブ数を表示する画像 URL はレスポンスに Access-Control-Allow-Origin: https://b.hatena.ne.jp
を含むので例として使用している。
コード例1 (画像を取得できる)
<img src="https://b.st-hatena.com/images/users/gif/normal/00001.gif"> // OK
コード例2 (これは駄目)
fetch("https://b.st-hatena.com/images/users/gif/normal/00001.gif"); // CORS エラー
コード例3 (crossorigin 属性を付与すると駄目)
<img src="https://b.st-hatena.com/images/users/gif/normal/00001.gif" crossorigin="anonymous"> // CORS エラー
Chrome の Network タブを見てみると、コード1 の場合はリクエストヘッダーに Origin が含まれていないが、コード2,3 の場合は含まれている。
これは、MDN の Origin に関する記述から引用すると
There are some exceptions to the above rules; for example if a cross-origin GET or HEAD request is made in no-cors mode the Origin header will not be added.
ブラウザは、Origin ヘッダーを基本的には付与するものの、いくつもの例外が存在し、no-cors モードのリクエストには付与しないという。
no-cors モードは、すなわち img タグが(crossorigin 属性を付与しない限り)クロスオリジンでリクエストを行う場合に該当する。 Origin ヘッダーがなければ、たとえレスポンスに Access-Control-Allow-Origin ヘッダーが含まれていても許可されている Origin なのか判定できないから CORS 違反にはならないということだろうか?
ちなみに、fetch を利用して img タグのリクエストを再現すると以下になり、この場合は CORS 違反とはならない。
コード4
fetch("https://b.st-hatena.com/images/users/gif/normal/00001.gif", { mode: "no-cors", });
mode: no-cors の意味がやっと分かった気がする。
ちなみに、img タグを使われると画像配信側はクロスオリジンリクエストでレスポンスを必ず読み取られてしまうのか、というとそんなことはない。
Origin 解体新書の Origin をまたぐその他の仕様 にも記載があるが、サーバー側でリクエストのヘッダーに Origin が含まれているのか判定し、403 エラーを返す実装にすることで実現できる。
こうすると、コード1 のように crossorigin 属性が未付与の場合は Origin ヘッダーが含まれないので 403 エラーになるし、コード3 のように Origin ヘッダーを含めても許可されていない Origin は Access-Control-Allow-Origin に許容すべきオリジンを列挙してレスポンスに含めることでブラウザはレスポンスを読み取れない。
参考
https://stackoverflow.com/questions/47978252/how-img-tag-gets-content-over-cors-headers