Web開発初心者が引っかかるCORS。私はいまだにちゃんと理解できていなかったなと感じたので、ちゃんと分かるようになるまで調べた倒しました!なるべく初心者にもわかりやすいように、調べた内容をまとめてみたいと思います。
CORSってなんだ?
CORSとはCross-Origin Resource Sharing
のことです。オリジン間リソース共有、そのまんまの意味ですが、異なるオリジン間でリソースを共有する仕組みのことです。
ちなみにここでいうオリジンは
- プロトコル
- ホスト
- ポート番号
の3つで構成されています。
例えばhttps://localhost:8080なら
プロトコル →https, ホスト →localhost, ポート番号 →8080です。
ここでは例えばローカルホストで起動したフロントエンドで、とあるAPIにアクセスしてデータ取得した結果を画面に表示したいとします。
その時の前提としては、
フロントエンド:https://localhost:3000
アクセス先API:https://localhost:8080
という設定でアクセスしようとするとします。
すると、以下のようなCORSエラーが出ます。
というのは、アクセスされるAPI側でCORSの設定をしていないので、フロントエンドの違うオリジンからアクセスしようとした時にはじかれてしまうわけです。
プリフライトリクエストって何のため?
CORSについて調べていると出くわすのがプリフライトリクエストというCORSの一部であるリクエスト。MDNによれば、プリフライトリクエストを送ることで、
サーバーが CORS プロトコルを理解していて準備がされていることを、特定のメソッドとヘッダーを使用してチェックします。
どういうことかというと、
クライアントサイド:「DELETEメソッドをこれから送っても良いですか?」
サーバーサイド:「いいよ!」もしくは「許可していません!」
というやり取りのこと。
ちなみにこのプリフライトリクエストは勝手に送られますが、以下の単純リクエストの際には送られません。
単純リクエストは、以下のすべての条件を満たすものです。
許可されているメソッドのうちのいずれかであること。
GET
HEAD
POST
ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で禁止ヘッダー名として定義されているヘッダー)を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で CORS セーフリストリクエストヘッダーとして定義されている以下のヘッダーだけです。
Accept
Accept-Language
Content-Language
Content-Type (但し、下記の要件を満たすもの)
Content-Type ヘッダーでは以下の値のみが許可されています。
application/x-www-form-urlencoded
multipart/form-data
text/plain
XMLHttpRequest オブジェクトを使用してリクエストを行う場合は、 XMLHttpRequest.upload プロパティから返されるオブジェクトにイベントリスナーが登録されていないこと。すなわち、 XMLHttpRequest インスタンスを xhr とした場合、どのコードも xhr.upload.addEventListener() が呼び出してアップロードを監視するためのイベントリスナーを追加していないこと。
リクエストに ReadableStream オブジェクトが使用されていないこと。
ただ、単純に思ったのですが、なぜCORSにはこのプリフライトリクエストが必要なんでしょうか。メソッドのチェック程度じゃ、このリクエストを送るのに何の意味があるんでしょうか?これについてはドンピシャな回答をstackoverflowで見つけたので共有したいと思います。
これによると、プリフライトリクエストはセキュリティ対策というよりも、ルールを変えないための仕組みとのこと。具体的に言うと、CORSを意識したことがないサーバに対して利点がある仕組みで、クライアントとサーバ同士でCORSが意識されているかという健全性チェックとして使われています。
例えば以下のようなユースケースでプリフライトリクエストの有効性についてみることが出来ます。
-
CORSが開発される前に作られたサーバ。クロスドメインのDELETEリクエストなんて来ないという前提で作られているため、既に悪意のある攻撃を受けている可能性もあり。ただこういったサーバがCORS/プリフライトリクエストのある世界にある場合、プリフライトリクエストを送る健全性チェックをすることで、ルールが変わったことによってクライアントとサーバーがぶっ壊れるのを防いでくれます。
-
開発中のサーバでまだ古いコードを使っていて、これがクロスドメインできちんと健全に動作するのかチェックできていない場合。このシナリオでは徐々にCORSをオプトインさせることが出来ます。例えば、「これからこの特定のヘッダーを許可します」「これからHTTPの特定の動作を許可します」「これからCookie/Auth情報が送られるのを許可します」といった具合です。
-
CORSを意識している新しいサーバーではちょっと異なります。標準セキュリティポリシーではサーバーはいかなるリクエストに対してもリソースを守らなければなりません。サーバーはクライアントが悪意のあるリクエストを行わないということは信用しません。このシナリオではプリフライトは特に役に立つというわけではありません。ここではプリフライトリクエストがあっても、特に更なるセキュリティ強化にはなりません。
参考:
・What is the motivation behind the introduction of preflight CORS requests?
CORSの歴史
CORSが出来る前はどういう仕組みになっていたのか?ですが、その昔は異なるオリジン間でデータのやり取りなどは出来ないようになっていました。(つまり外部のAPIを叩いてデータを取得するなんてことはもっての外、自作のAPIやサーバーサイドから取得するのが前提だったわけです)
ただ、今は普通に出来ますがそれが当たり前でなかった時代のことを考えるととてつもなく不便だということは分かります。
ですがもしCORSがなかったらどうなるでしょうか?
例えば悪意のあるサイトURL、http://maliciousweb.com というのがあったとして、間違ってユーザーがこのURLを叩いてしまったとしましょう。ここからユーザー情報を取得するようなAPIリクエストが投げられてしまい、CSRF攻撃が可能になってしまいます。
CORS設定時の注意
これにはGoogleが出しているCORに関するWikiが大変詳しく分かりやすく説明されていたので、こちらを参考にしています。
Guide to Secure Implementation of HTML5's Cross Origin Requests
・全面許可
あ!CORSエラーだ!よし、Access-Control-Allow-Origin: *
、OKっと。で設定していたりしませんか?これをすると、すべてのウェブサイトからアクセスし、レスポンスを読むことが出来るようになります。で、もし外部からアクセス不可能なウェブサイトの一部がこういったワイルドカード許可オプションをしていた場合にユーザが攻撃者のウェブサイトを間違って踏んでしまうと、攻撃者のサイトからCORをしてこの内部リソースに入り、レスポンスを読んで盗むことが出来てしまいます。
・サイトレベルCOR
CORは設計上全てのページでアクセスコントロールが必要になります。そのページが明示的に許可していた場合のみ外部からのアクセスが可能になります。ただこれにもデメリットがあります。例えばCORで10のWebページからのアクセスを許可するという設定をしていた場合、Allowed-Orginのリストに変更があれば、その10のページ全てに変更を入れないといけない面倒さがあります。
・オリジンヘッダーに応じてアクセスコントロールを決める
オリジンヘッダーには一定量の信用要素が入っています。ただこれがサーバー側できちんと理解されなかった場合、間違いが起きてしまうこともあり得ます。オリジンヘッダーではそれが特定のドメインからのリクエストであることしか分かりませんし、その事実について保証しているわけではありません。例えば、そのリクエストはオリジンからのリクエストを装ったPerlスクリプトかもしれません。
・プリフライトリクエスト結果を保存するキャッシュの時限延長
単純リクエスト以外のリクエスト送信時にはプリフライトリクエストが送られるわけですが、単純リクエスト以外のリクエストを何度も送らないといけない場合、それ自体がパフォーマンスネックになってしまうことがあります。それを防ぐためにプリフライトリクエストの結果をキャッシュに保存する時間をAccess-Control-Max-Ageのヘッダーに設定する方法があります。ただ、設定するのは30分ぐらいにしておいた方がよさそうです。というのも、長時間保存するようにした場合CORの設定をサーバーで変更したとしても、古いキャッシュにあるプリフライトリクエストの情報を使ってしまうからです。
・信用情報をどこに置くか
例えばTwitterとFriendfeedがあって、FriendFeedがツイートを読んだり、ツイートしたり、その他のアクションを行うためにCORをTwitterに送るとします。TwitterがFriendFeedからのCORを許可してそれらを処理してレスポンスを送ります。両者は互いに信用しているため、FriendFeedはTwitterからのデータを有効化したり、TwitterがFriendFeedに対してCOR経由でほぼ全ての機能を公開しているとします。
シナリオ1 - Twitterが妥協する
FriendFeedはTwitterがつねにHTMLエンコードでデータを送ってくるので、特にエンコードしたり有効化したりせずそのままデータをサイトに公開します。ここでTwitterが妥協してしまっているため、攻撃者が悪質なフィードを含む生HTMLとJavascriptを送ることができてしまい、Friendfeedのユーザーも犠牲になってしまいます。
シナリオ2 - FrinedFeedが妥協する
TwitterはCOR経由でFriendFeedに沢山の機能を公開しています、例えばツイートを送信したり、ユーザ名を変えるというのもこれに含まれます。FriendFeedが妥協すると攻撃者はCORをTwitterに送りユーザを装って例えば悪意のあるツイートをツイートするような行為を行うことが出来ます。
リクエストを送る側が受け取ったデータを有効化したり、サーバー側では最低限必要な機能のみ公開するという対策は重要です。
・不正COR処理
単純なCORであれば、サーバがCORを許可していなくてもそのページに対して送信してしまうことは可能です。CORにさらされていないページでも不正CORsを受け取ることはあり得ます。同様に信用しているサイトリストからのみCORsを受け取るということにしているページでも別のオリジンからの不正CORsを受け取ることが出来てしまいます。こうした不正CORsは時にアプリケーションレベルDDOS攻撃を行うことに使われることもあり、アプリケーション側で処理されるべきではありません。
例えば検索ページ「documentsearch.php」で「%」を検索した場合サーバーは該当するすべてのレコードを返してしまうかもしれず、そうすると計算がかなり重いリクエストになってしまいます。このサイトをサーバーダウンさせるためには、攻撃者はパブリックな掲示板を使って被害者のブラウザに対して何度もこの検索を行うようなJavascriptインジェクションを入れるXSS脆弱性が含まれる内容を書き込むかもしれません。
もしそのページがほかのドメインからアクセスされるべきでないなら、リクエストの度にオリジンヘッダーを確認し、リクエストを拒否するべきでしょう。これはWAFでも可能です。
最後に
いかがでしたでしょうか。CORSについて、少しでも理解の助けになれば幸いです。