imgタグとJavaScriptそれぞれで同じ画像にアクセスした際に Chrome でのみ CORS エラーになる場合の対処

2021.12.30

西田@大阪@MAD事業部です

今回は Chrome で1つの画像を同じURLで <img> タグと JavaScript とそれぞれでダウンロードした時に、JavaScriptでのリクエストがCORSエラーになる現象に遭遇したので、その原因を調査しました

現象

WEB(上記図でweb.example.com)から ASSETS (上記図 で assets.example.com) に <img> タグの src 属性に設定して画像をダウンロードした場合は成功して、 JavaScript で fetch関数等を用いてダウンロードした場合は CORS Error になってしまいます

原因

Chrome が<img>タグで画像をダウンロードした際にHTTPヘッダも含めたレスポンスをキャッシュし、JavaScriptでダウンロードしようとした際にHTTPヘッダも含めてそのまま返してしまうのが原因でした。これは Firefox など他のブラウザでは再現しません

WEB からJavaScriptで ASSETSにアクセスするにはクロスオリジンのため、CORS関連のHTTPヘッダが ASSETSから返却される必要があります。

しかし、画像を <img>タグで取得する際には、CORSが必要ないためChromeはOrigin ヘッダを送信しません。

Originヘッダが送信されないと、S3はCORSの設定されていても CORS 関連のHTTPヘッダをレスポンスに含めません。参考

ChromeがHTMLを描画時、<img>タグで画像をダウンロードした際に CORS ヘッダがないレスポンスをキャッシュし、2回目以降のJavaScriptでのダウンロード時に画像をサーバーに取得にいかず、CORSのHTTPヘッダがないキャッシュされたレスポンスを返してしまいます

エラーになる具体例

イメージを具体的にしてもらうために、例を上げると、以下のHTMLをChromeで表示し <button>をクリックした際にCORSエラーになります

<img src="https://assets.example.com/hanako.png"/>
<button onclick="fetch('https://assets.example.com/hanako.png')">fetch</button>

対策

<img>タグに crossorigin属性をつけることで解決します

<img src="..." crossorigin="anonymous">

crossorigin属性をつけると、 <img>タグで画像をダウンロードするリクエストにも Originヘッダをつけて送信するようになり、CORS関連のHTTPヘッダがある状態でキャッシュされるようになります

crossorigin属性について

最後にcrossorigin属性について少しハマったので補足しておきます

crossorigin属性に設定できる値

説明
anonymous crossorigin 属性を設定した要素のリクエストの資格情報モード(credentials mode) が same-origin になります
use-crendentials crossorigin 属性を設定した要素のリクエストの資格情報モード(credentials mode)が include になります

参考: HTML crossorigin 属性 - HTML: HyperText Markup Language | MDN

anonymous

Cookie、クライアント証明書、認証ヘッダーの情報をSame Originの場合のみ送信します。

理解のために実際に動かして確かめていきます

画像を配信している、ASSETS側のドメインに Cookie 設定用の以下のHTMLを配置し、ブラウザからアクセスします

<script>document.cookie = "test2=hello;SameSite=None;Secure"<script>

この状態で、画像のURLをそのままブラウザで開くと、 Cookie が送信されていることが確認できます

※ Same Origin だと crossorigin 属性を設定しなくても Cookie は送信されます

Cookie が設定された状態で ASSETS の画像が埋め込まれている WEBのHTMLをブラウザで開くとリクエストに Origin ヘッダが含まれているが、Cookieが含まれていないことが確認できます

<img src="..." crossorigin="anonymous">

use-credentials

Cookie、クライアント証明書、認証ヘッダーの情報をクロスオリジンでも送信します

anonymous の時と同じようにして Cookie を保存します

その状態で crossorigin 属性に use-credentialsを設定し、WEBのHTMLの中に埋め込みアクセスします

<img src="..." crossorigin="use-credentials"/>

すると、 originヘッダに加え、Cookieも送信されている事が確認できます

ただし、 use-credentialsを設定している時に Access-Control-Allow-Origin* が設定されている場合以下のエラーになりました

web.example.com Access to image at 'https://assets.example.com/hanako.png' from origin 'https://web.example.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

これは資格情報モード(credentials mode)が includeの時に Acccess-Control-Allow-Origin*(ワイルドカード)が設定できないためです。

参考 https://fetch.spec.whatwg.org/#cors-protocol-and-credentials

*をやめて正しくOriginを指定する必要があります

今回の例で言うと https://assets.example.comと指定し * がブラウザに返却されないようにする必要があります

参考

特定の条件でのみS3の画像URLでCORSエラーが発生する問題をなんとかする - Qiita