CORS を実験的にテストできるアプリ
概要
CORS を何度学んでみても、しばらく経つと全く内容を理解していないことが多かったので、ローカルで動作する CORS を試すことのできるアプリケーションを作って触ってみました
※ 自分のブログでも同じ記事を書いています
CORS の概要
CORS とは javascript が本来禁止されているクロスオリジンのリソースにアクセスするための制限緩和のためのブラウザの仕組みです
本来、javascript は同一オリジンポリシー(SOP)というセキュリティ上の制限があり、異なるオリジンのリソースにアクセスすることができませんが、
現代ではアプリケーションがオリジンを跨いでアクセスすることが一般化しているので、CORS での制御が必要となります
あくまで「JavaScriptによるデータの読み取り」に対する制限緩和の仕組みであ離、iframe や img タグなどによる「HTMLの埋め込み表示」 には適用されません
オリジンとは
スキーム + ホスト + ポート の組み合わせのことです
以下は全て異なるオリジンとなります
http://localhost:3000-
http://localhost:8080(ポートが違う) -
https://localhost:3000(スキームが違う)
そもそも何で同一オリジンポリシーがあるのか?
そもそも何でこの SOP が「Javascript」にあるのかは、
「Javascript」と「HTMLの埋め込み表示」を比較すると理解しやすいです
-
Javascript: 動的にデータを操作できるため、他サイトの秘密データを読み取り、それを別のサーバーへ転送できてしまう(読み取り&持ち出しが可能)
-
HTMLの埋め込み: 静的な表示機能であるため、単に画面に表示するだけで、その中身のデータを解析して別のサーバーへ転送することはできない
つまり、js というプログラムからデータを読み取れてしまうと、
悪意のあるサーバーへ別オリジンの個人情報などを転送できてしまうので、これを防ぐためにあるわけです
CORS の仕様
CORS はAccess-Control-Allow-*といったヘッダーを利用して、アクセスを許可するオリジン・操作・ヘッダーなどを許可を見た上でリクエストを送信できるかを決定します
そのヘッダーをまずもらう必要がありますが、許可の確認をするための CORS のリクエストには、以下2種類のリクエストが存在します
- プリフライトリクエスト
- シンプルリクエスト(単純リクエスト)
プリフライトリクエスト
1つ目の「プリフライトリクエスト」の方式では、ヘッダーを確認するためのリクエストをブラウザが事前に勝手に送信してくれます
このリクエストで、別サーバーがクロスオリジンでのアクセスを許可しているオリジンや対象メソッド・ヘッダーをブラウザが確認し、安全にリクエストが送信できるのか確かめた上で送信をしてくれる仕様となっています
シンプルリクエスト(単純リクエスト)
一定の条件を満たすリクエストは、事前確認をせずに直接クロスオリジンのリクエストを送信することができます(GET,POSTなど該当するのでこちらの方がよく使われるはず)
ただし、事前確認(プリフライトリクエスト)をしないとはいっても、返却されたヘッダーで CORS の許可の確認はしています
許可されていないメソッドやオリジンであれば、javascript から読めないようにブラウザがレスポンスを破棄しているだけで、リクエスト自体は成功してレスポンスは返ってきています(開発者ツールなどを使うとレスポンスが見える)
シンプルリクエストが発生する詳細な条件は以下参照
オリジン間リソース共有 (CORS) - HTTP | MDN
なぜシンプルリクエストでは、プリフライトリクエストでの事前確認をしないのか?
これは、昔からある HTML の<form>で送信できるGETやPOSTなどのリクエストでは、同一オリジン外からの悪意あるリクエストの対策(CSRFの対策)をしていることが前提であり、「事前にクロスオリジンの通信を防ぐように対策をしてるよね?だから Javascript の CORS で面倒は見ないっすよ」的な話らしいです
アプリケーションの概要
ここからアプリケーションを用いて、実際に挙動を見てみます
アプリケーション自体は大体AIに書かせて細々とした修正だけしています
- バックエンド/フロントエンドともに Typescript
- バックエンド
-
itemsというテスト用データをメモリ上に持ち、フロントエンドから各種メソッドを用いて操作可能- デフォルトで
id:1,2のデータが用意されている
- デフォルトで
- 別途、フロントエンドに返却する CORS 設定値を操作するAPIを持つ
-
Cache-Control: no-storeとしており、ブラウザがキャッシュを持たないようにしている
-
- フロントエンド
- バックエンドに対して各種API操作が可能
- CORSの設定も変更可能
デフォルトのitemsデータ
[
{"id":1,"name":"item1","value":"foo"},
{"id":2,"name":"item2","value":"bar"}
]

CORS 許可設定なしの挙動
シンプルリクエスト
GET メソッドを実行すると、CORS エラーが返ってきます
Response もFailed to fetchとエラーとなっています

中身を見てみると、レスポンス自体は正常に 200 となっているため、ブラウザがレスポンスを捨てたことがわかります

POST も同様に、エラーが発生します

ただし、このエラーの中身を見てみると、POST 自体は実行されてid:3の新しい item が追加されています
先述のシンプルリクエストの項の通り、あくまでブラウザが結果を破棄しているだけなので、
CORS を制限をしていてもリクエスト自体は実行されてしまいます
CSRF は、CORS を制限するだけでは防げないということです

プリフライトリクエスト
PUT を実行してみると、以下の2つのリクエストが飛んでいることが分かります
-
preflightリクエスト(CORSエラーとなっている) - CORS エラーのリクエスト
PUT はプリフライトリクエストが飛ぶメソッドですので、挙動が正常なことが分かります

PUT 自体がでは実行されたのかというと、itemsはデフォルト値のまま残っていて変更されていません
つまり、プリフライトリクエストが発生する場合には、メソッドの実行自体が制限されていることがわかります

中身を見てみると、ステータスコードが返ってきていないので、リクエストが送信されていなさそうですね

まとめ
以上より、CORS はリクエストの実行自体を防ぐというよりは、あくまで別オリジンのデータを javascript が読みとって悪意のあるユーザーの元へ送るのを防ぐような役割を持っていることがわかります
悪意のあるリクエストの実行自体を防ぐものではない、ということは念頭に入れておく必要がありそうです(一部の CSRF を防止してくれるものであり、XSS は防げない)
Discussion