久しぶりにハマったので備忘録を残しておきます
問題
CloudFrondを使っている環境において、CORS (Cross-Origin Resource Sharing) に該当するリクエストが成功したり失敗したりしました。
前提の共有
サーバサイドにおけるCORSの挙動は2種類あります。
- どんなリクエストに対してもCORSの設定を含んだHTTPレスポンスを返却する
- HTTPリクエストにOriginヘッダがあった場合に、CORSの設定を含んだHTTPレスポンスを返す。
nginxとかrailsのアプリケーションで適当に設定してしまう場合は面倒なので1番めになります。AWS S3で設定を行う場合は2番の設定になります。
そのため、2番の設定を検証するためにはリクエストにOriginヘッダを含めて検証する必要があります。
% curl -i 'https://postcode.teraren.com/postcodes.json' -H 'Origin: http://example.com' 2>&1|grep -i access access-control-allow-origin: * access-control-allow-methods: GET, HEAD
また、1の場合はCDNにおいて一律クライアントに返却する場合と、CDNのorigin側でレスポンスを返すという2パターンのデプロイ方法が存在します。
このあたりのポリシーを決めた上で、設定していく必要があります。この設計によってトラブルシューティングする場所が大幅に変わってきます。
問題が起きるケース
2時間ぐらいトラブルシュートして、やっと問題がわかりました。CloudFrontをほぼデフォルトで設定した場合に起きる、以下のようなケースのときです。
- HTTPクライアントがOriginヘッダを付けないでCloudFrontにリクエスト
- CloudFrontにhttpレスポンスヘッダも含めてキャッシュされる。(CORSのレスポンスが入らないキャッシュができる)
- HTTPクライアントがOriginヘッダを付けてCloudFrontにリクエスト
- CDNがキャッシュ済みのオブジェクトを返却。(これにはCORSが入っていない)
対処方法
CloudFrontのBehaviorsの設定において、CDN上のキャッシュのキーにHTTPリクエストの”Origin”ヘッダを含めるようにする。
Elemental-MediaPackage
Managedポリシーの中で唯一Originヘッダをキーとして使うルールです。他にも細かい設定が入っているのですが、面倒なのでこれを使います。
CORS-S3Origin
HTTPクライアントから送られてきたリクエストをOriginに送るヘッダです。
- origin
- access-control-request-headers
- access-control-request-method
PoC
// This would succeed if CORS setting is correct. fetch("https://postcode.teraren.com/favicon.ico") .then((res) => res.blob()) .then((blob) => { let image = new Image(); image.src = URL.createObjectURL(blob); image.width = 200; image.height = 200; document.getElementById("cf").appendChild(image); }) .catch((error) => { console.error("通信に失敗しました", error); });
参考資料
余談1
BraveやChromeのDeveloper Consoleでトラブルシュートをしているときに、Networkタブにて確認しているとがOriginヘッダ無しのリクエストを出したり、Originヘッダ付きのリクエストを出したりしていました。
よくよく確認してみると、Javascriptからの呼び出しは1回なのに、リクエストが2回飛んでいました。1つは、意図した通りのリクエスト、もう1つは単にブラウザがGETしているだけのリクエスト。後者を呼び出すようなコードを書いていないので意味不明だった。
トラブルシューティングをする際に、上記の2種類のリクエストのどちらをチェックするかによって変わってくるのでそこをしっかり確認しておく必要があります。
→原因判明。上記のPoCのコードを
image.src = "http://postcode.teraren.com/favicon.ico";
と書くと、CORSを考慮しない普通のGETリクエストが走ることになり、fetchとは別のリクエストが飛びます。しかしながら、ブラウザのキャッシュのメカニズムが、URLをキーとしたキャッシュになるのでCORS無しのリクエスト、レスポンスがキャッシュから呼ばれてしまうときがあります。
よって、1ページ内で外部リソースを読むときに、CORS付きでリクエストをするコードと、CORS無しでリクエストをするコードが存在する場合、予期しない挙動になります。
自分でスクラッチから書いていれば問題ないと思いますが、ライブラリレベルで外部リソースの呼び出し方が異なる場合はかなり面倒なことになります。どちらかのライブラリを使わないようにする解決方法しか無いのかなと思います。
余談2
かつて、メルカリがCDNの設定をミスって情報漏洩をしたこともあります。
CDNの設定は、アプリケーションごとにいろいろなユースケースを考えてテストをしないといけないので、なかなか難しいです。
Comments