【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のいずれかであること - 手動で設定できるリクエストヘッダは以下のいずれかであること(ユーザエージェントによって自動的に付与されるヘッダーを除く)
AcceptAccept-LangageContent-LangageContent-Type
-
Content-Typeのヘッダは以下のいずれかであることapplication/x-www-form-urlencodedmultipart/form-datatext/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