👏

【CORS】なぜ、Preflight Request が発生するときとしないときがあるのか

2024/06/21に公開

よく聞くこの Preflight Request

これってなにしてんだろ?って思ったことありません?

こういうHTTP MethodOPTIONSになっているやつ

セキュリティを担保するうえで重要な Preflight Request
どうやら毎回必ず発生しているものではないらしいです。

そこらへんの疑問を解消していきたい。

OriginとかSame Origin Policyに関してあやふやな方はこちらを読んでざっくり理解してから、先に進むことをおすすめします。
https://zenn.dev/tm35/articles/3eeb44f5e3ec8a

■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 とは

CORSCross-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 HeadersAccess-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 HeadersAccess-Control-Allow-Originは設定されていない状態です。

②Preflight で CORS 許可されてないことがわかると、メインのリクエストは送信されない

Preflight RequestResponse 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

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#単純リクエスト

この条件をみたせば<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 PolicyCORSの登場するタイミングによる違いがあるようです。

  • 1990年代半ば: Same Origin Policy 導入
  • 2008年: CORS 標準化

結構長くなってしまいましたが、CORSPreflight Requestの役割とその仕組み、発生する条件やしないケースについて理解を深めることができました!

ありがとうございました!

https://developer.mozilla.org/ja/docs/Glossary/CORS
https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy
https://zenn.dev/jxck/books/origin-anatomia
https://zenn.dev/tm35/articles/3eeb44f5e3ec8a
https://zenn.dev/syo_yamamoto/articles/445ce152f05b02
https://zenn.dev/qnighy/articles/6ff23c47018380

https://www.youtube.com/watch?v=ryztmcFf01Y
https://www.youtube.com/watch?v=yBcnonX8Eak

Discussion