【CORS】なぜ、Preflight Request が発生するときとしないときがあるのか
よく聞くこの Preflight Request
これってなにしてんだろ?って思ったことありません?
こういうHTTP Method
がOPTIONS
になっているやつ
セキュリティを担保するうえで重要な Preflight Request
どうやら毎回必ず発生しているものではないらしいです。
そこらへんの疑問を解消していきたい。
Origin
とかSame Origin Policy
に関してあやふやな方はこちらを読んでざっくり理解してから、先に進むことをおすすめします。
■Preflight Request とは
Preflight Request
はブラウザが自分自身とは異なるOrigin
のサーバにリクエストする際、事前にブラウザから自動で送信されるものです。
◎なんのために Preflight Request が送信されているのか
Preflight Request
はサーバが対象のリクエストを受け付ける許可をしているかを確認するために送信されます。
◎異なるOrigin からのリクエストは Same Origin Policy によってブロックされる
基本的に異なるOrigin
からのリクエストはSame Origin Policy
によってブロックされます。
つまり、どこのどいつかわからないサイト(Origin
)からのリクエストは基本受け付けないような仕様になっているのです。
*異なるOrigin
関係のことをクロスオリジン(Cross-Origin
)という
◎信頼したサイトからはアクセスできるようにしたい
しかし、異なる Origin であっても信頼しているサイト(Origin
)からはアクセスができるようにしたい
という、要望があります。
そこで登場したのが、CORS(Cross-Origin Resource Sharing)
です。
CORS
設定により許可されているOrigin
に対してはリクエストがブロックされなくなります。
■CORS とは
CORS
(Cross-Origin Resource Sharing
, オリジン間リソース共有)
CORS
とはリクエストを送信しようとしている自分自身(HTML
, スクリプトなど)のOrigin
とは別の Origin
からのリクエストに対してリソース共有を許可するかどうか決定する仕組みのこと
先述したように別Origin
にリソース共有を許可していない場合、基本的にSame Origin Policy
によりリクエストはブロックされます。
この仕組みはブラウザがデフォルトでもっているものです。
では、ブラウザはどのようにサーバでCORS
許可がされているかを判断しているのでしょうか?
◎ブラウザはどうやって CORS 許可を判断しているのか
ブラウザはサーバーから返ってきた、Response Headers
からCORS
許可を判断しています。
つまり、
サーバからレスポンスを受け取ってはじめて、レスポンスを利用すべきかをブラウザが判断しているのです。
◎ブラウザは Response Headers を見ている
このように異なるOrigin
関係のサーバとブラウザで考えてみます。
サーバ: http://localhost:8080
ブラウザ: http://localhost:3000
このときサーバ: http://localhost:8080
としては、ブラウザ: http://localhost:3000
からのリクエストは信頼できるとわかっているので、異なるOrigin
であってもリクエストを受け付けるようにしたい。
その場合、サーバ側でhttp://localhost:3000
からのリクエストを許可設定をすることで、リクエストが成功し無事レスポンスを利用することができます。
// サーバ側で Response Headers に設定する
Access-Control-Allow-Origin: http://localhost:3000
このときResponse Headers
でAccess-Control-Allow-Origin: http://localhost:3000
が指定されていないとブラウザがエラーを発生させてレスポンスを使うことはできません。
◎Response を見てから判断するだと遅いのでは?
ここで疑問が湧きます。
この仕組みだとレスポンスを見てからエラーを発生させるので、サーバでリクエストは受け入れられて処理されてしまいそうです。
今回のようにGET
リクエストであれば問題なさそう。
しかし、POST
, DELETE
, PUT
などはデータが変更されるような副作用が発生する ケースが多いです。
つまり、悪意あるサイトからPOST
, DELETE
, PUT
リクエストが送られてきた場合、ブラウザがレスポンスを利用できずとも副作用が発生させられてしまう危険性があります。
そこで登場するのがPreflight Request
です。
◎ Preflight Request は事前チェックをしている
Preflight Request はメインのリクエストを送信しても問題ないかを事前に確認するために送信されます。
今度はサーバ: http://localhost:8080
としては、ブラウザ: http://localhost:3000
からのリクエストは許容したくないとします。
- メインのリクエスト:
DELETE http://localhost:8080
- Preflight リクエスト:
OPTIONS http://localhost:8080
①Preflight を送信して Response を確認する
まず、OPTIONS
リクエストを送信します。
このときResponse Headers
にAccess-Control-Allow-Origin
は設定されていない状態です。
②Preflight で CORS 許可されてないことがわかると、メインのリクエストは送信されない
Preflight Request
のResponse Headers
からCORS
が許可されていないことを確認するとメインのリクエストは送信されません。
このPreflight Request
の仕組みにより、副作用があるPOST
, DELETE
, PUT
などのリクエストもCORS
の仕組みによってブロックされることになります。
■Preflight Request が発生しないケースがある
異なるOrigin
間でブラウザが自動で送信しているPreflight Request
。
しかし、異なるOrigin
間であってもこのPreflight Request
が発生しないケースがあります。
どんなケースだと発生しないのでしょうか?
◎CORS 以前から存在するリクエストには Preflight が発生しない
<form>
からのリクエストなど、CORS
以前から存在している Same Origin Policy の制限を受けないリクエストに対しては Preflight Request
は発生しません。
なぜなら、CORS
登場以前から可能だったRequest
に対しPreflight Request
を送るようにすると既存で許容していたアプリケーションに不具合が生じる可能性があるからです。
○なぜ CORS 以前から存在するリクエストに Preflight を送信してはいけないのか
例えば、このような決済フォームがあり、異なるOrigin
にリクエストを送信できるとします。
このフォームはCORS
以前から存在し決済処理をしていました。
// ブラウザの Origin は https://shopping.example.com/
<form action="https://payment.example.com/submit" method="post">
<input type="hidden" name="amount" value="100">
<input type="submit" value="決済">
</form>
このフォームがCORS
登場後Preflight Request
を送信します。
するとサーバ側でのCORS
対応が追いついていなければCORS
エラーのためメインのリクエストが送信されないことになります。
つまり、CORS
以前の仕様に対して互換性がなくなってしまうのです。
○Preflight が発生しない Simple Request
そして、このような CORS
以前から異なるOrigin
に対するリクエストが許容されていたリクエスト形式のことを Simple Request や単純リクエストと呼ぶようです。
これら形式のリクエストにはPreflight Request
は発生しません。
どうやら、このCORS
以前から存在しているSimple Request
に対してはすでにCSRF
などのセキュリティ対策が行われていると仮定しているようです。
そのため、CORS
登場によって新しくもたらされるセキュリティリスクではないと判断し、Simple Request
に対してはPreflight Request
が発生しません。
Simple Request の条件
覚える必要はないですが、これらの条件すべてをみたすものがSimple Request
です。
- 許可されているメソッドが
GET
,HEAD
,POST
のいずれかであること - 手動で設定できるリクエストヘッダは以下のいずれかであること(ユーザエージェントによって自動的に付与されるヘッダーを除く)
Accept
Accept-Langage
Content-Langage
Content-Type
-
Content-Type
のヘッダは以下のいずれかであることapplication/x-www-form-urlencoded
multipart/form-data
text/plain
この条件をみたせば<form>
でなくfetch()
であってもPreflight Request
は送信されません。
// Preflight Request は送信されない
fetch('https://payment.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
// ↑↑ は ↓↓ と同じリクエスト形式
<form action="https://payment.example.com/submit" method="post">
<input type="hidden" name="amount" value="100">
<input type="submit" value="決済">
</form>
■さいごに
なぜこのようにCORS
の仕様が複雑になっているかというと、Same Origin Policy
とCORS
の登場するタイミングによる違いがあるようです。
- 1990年代半ば:
Same Origin Policy
導入 - 2008年:
CORS
標準化
結構長くなってしまいましたが、CORS
やPreflight Request
の役割とその仕組み、発生する条件やしないケースについて理解を深めることができました!
ありがとうございました!
Discussion