きっかけ
業務で、Next.jsのフロント側からLaravelのAPIを叩いた時にCORSエラーというのが出ました。
エラー解消法はすぐに見つかったものの、セキュリティやその対策の仕組みについてはきちんと理解して実装したいと思い、色々調べたのでまとめておきます。
同一オリジンポリシー
CORSについて理解するためには、前提として「同一オリジンポリシー」について知る必要があります。
オリジンとは
ドメイン+プロトコル+ポート番号 を合わせたもの。
【例】
・ドメイン (domain):yahoo.co.jp
・オリジン (origin): https://yahoo.co.jp:443
「同一オリジンである」とは
ドメイン・プロトコル・ポート番号の全てが一致している場合のこと。
同一オリジンポリシー(Same-Origin Policy)とは
MDNには以下のように書いてあります。
同一オリジンポリシーは重要なセキュリティの仕組みであり、あるオリジンによって読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアクセスできる方法を制限するものです。
要するに、別のオリジンへのアクセスに制限をかけることで、XSSやCSRFといった攻撃を防ぐことを目的とするセキュリティの仕組みです。
XSSとCSRFについてそれぞれの詳しい攻撃手法はここでは取り上げませんが、対策を講じる上での要点を挙げておきます。
XSS (Cross Site Scripting)
ユーザーが 意図しない不正なスクリプトが Webサーバーに送られ、Webサーバーからのレスポンスを受け取ってしまった結果クライアント側 (Web ブラウザ) で実行される攻撃。
ブラウザ上で出来ることは何でも悪用対象になるので、CSRFより攻撃範囲が広い。
CSRF (Cross-Site Request Forgeries)
ユーザーが意図しない不正なスクリプトがWebサーバーに送られ、 Web アプリケーション (Web サーバー) 上で実行される攻撃。通称「しーさーふ」。
悪用内容はサーバー側で用意された処理に限定されるが、 サーバーにリクエストが到達するだけで攻撃が成立するので、XSS対策はできていてもCSRF対策は出来ていないということもあり得る。
JavaScript の組み込み API でありAjax 通信を実現する `XMLHttpRequest (XHR)` や `Fetch API` などは、これらの脆弱性を回避するため**同一オリジンポリシー**(別のオリジンへのアクセスに制限をかける仕組み)に従っています。
XSSとCSRFについての詳細は、以下の記事が分かりやすく勉強になりました。
CORS
同一オリジンポリシーについて抑えたところで、CORSについて見ていきます。
CORSとは
読み方:コルス or シーオーアールエス
Cross-Origin Resource Sharing の略で、日本語訳すると「オリジン間リソース共有」。
CORSは、あるオリジンで動いているWebアプリケーションから別のオリジンのサーバーへのアクセスを許可する仕組みです。
同一オリジンポリシーにより別のオリジンにはアクセスが出来ないという規制があるが、Web開発・制作では異なるオリジンにアクセスしたいケースもある…
そこで、同一オリジンポリシーの制約を回避・緩和してくれるのがCORSです。
歴史をたどるとCORSが必要になった経緯がさらによく分かります。
同一オリジンポリシーはあるが、CORSはない場合…(過去のブラウザ)
出来たこと
- 同一オリジンポリシーによりJavaScriptの安全性は確保される。
生まれた課題
- Ajaxの普及・発展により、異なるオリジン(主に異なるホスト)のAPIを呼び出したいという動機が生まれた。
- JSONPなど同一オリジンポリシーの範囲内で異なるオリジンのAPIを呼び出す方法が考案されたが、裏技のようなものであって安全性には課題が残っていた。
そこで生まれたCORS
上記のような課題を解決するため生まれたCORSは、以下の機能を提供します。
- クロスオリジンのアクセスを許可
- オリジン単位でのアクセス制御が可能(例:オリジンA・オリジンBとの通信のみ許可する)
- HTTPヘッダを用いてアクセス制御を行う
CORSはどうやってクロスオリジン通信を許可するのか
以下のように事前に通信を行う双方で設定を行っておくことで、クロスオリジンの通信が可能になります。
-
クライアントサイド
HTTPリクエストヘッダに
Originヘッダ
を付ける。- XHRの場合:自動でOriginヘッダが付くので何もしなくて良い
- FetchAPIの場合:mode: cors を付与する
-
サーバーサイド
HTTPレスポンスヘッダに以下を付ける。
※レスポンスヘッダを付ける方法は環境によって様々です。
Laravelの場合は、Laravel7.0以降は**config/cors.php
** を使ってCORSの設定ができます。*必須*
・Access-Control-Allow-Origin: アクセス元のオリジン
*必要な場合のみ*
・Access-Control-Allow-Credentials: true(Cookieを送信する場合は必要)
・Access-Control-Allow-Headers
・Access-Control-Request-Method
・Access-Control-Max-Age
CORSによるアクセス制御の流れ
実際にCORSがアクセス制御を行う流れを、ブラウザの検証ツールのネットワークを観察して確認します。
【前提】
・フロント側(Next.js)をlocalhost:3001、API(Laravel)をlocalhost:8080で開発中。
・localhost:3001のフロント側から、localhost:8080/logoutというURLにアクセスしてAPIをたたき、クロスオリジンの通信を発生させる。
・リクエストはXHRにより行うので、リクエストヘッダには自動的にOriginヘッダが付く。
・localhost:3001からのリクエストがあった場合は、レスポンスヘッダにAccess-Control-Allow-Originを載せるように、事前にAPI側で設定している。
【実際の流れ】
1. ブラウザでlocalhost:3001にアクセスする。
2. localhots:3001からlocalhost:8080/logoutへHTTPリクエストを送る。
<ポイント> リクエストヘッダのOriginという項目に、リクエスト元のドメイン情報が載せられる。
Origin: http://localhost:3001
3. レスポンスが返ってきたら、localhost:3001はlocalhost:8080からのレスポンスヘッダを見て、レスポンスを受け取るかどうか判断する。
<ポイント> レスポンスヘッダに Access-Control-Allow-Origin
という項目があり、自分のドメイン情報(localhost:3001)が載せられていればレスポンスを受け取る。
// レスポンスヘッダに以下の記載があれば、レスポンスを受け取る
Access-Control-Allow-Origin:http://localhost:3001
CORSエラーを起こしてみる
上記の例では、事前にAPI側(Laravelのconfig/cors.php)でlocalhost:3001からのリクエストに対してAccess-Control-Allow-Originを返す設定をしていた為、クロスオリジンの通信が成立していました。
試しにAPI側で設定を行っている箇所を削除してみると、レスポンスヘッダに Access-Control-Allow-Origin:http://localhost:3001
が無いので、CORSエラーが起こります。
ここでのエラーは、リクエスト自体は送っているけれど、レスポンスが返って来た時にそれを受け取らずレスポンスエラーということになっています。
プリフライトリクエスト
無条件でリクエストが飛んでも大丈夫なのか?
CORSの設定をしていないとエラーが発生して通信は成立しませんでしたが、そのエラーはあくまでレスポンスを受け取らないというものでした。
しかし、そもそもリクエストが飛んで良いのか?という懸念が残ります。
- 代表的なリスクがCSRF
CSRFではレスポンスを受け取る必要は無く、リクエストが送信できれば攻撃できる。
このようなリスク対策として、プリフライトリクエストが生まれました。
プリフライトリクエストの考え方
-
元々CORSが無いときにできていたクロスオリジンのリクエストに対して、大幅なリスク増にならない条件であれば、XHR等で無条件にクロスオリジンのリクエストを送信できるようにした。
-
「大幅なリスク増にならない」条件を単純リクエストとして定義した。
-
単純リクエストの要件を超える場合は、実際のリクエストを送る前にプリフライトリクエストを送り、実際のリクエストを送信して問題無いか事前に確認する。
-
プリフライトリクエストを挟むことで、プリフライトリクエストを送信した結果Access-Control-Allow-OriginのHTTPヘッダがついたレスポンスが返されなければ、実際のリクエストは送信しない、というように悪意あるリクエストを防ぐ。
単純リクエスト(Simple Request)の要件
以下の要件を全て満たす場合のみ、単純リクエストとなります。
-
メソッドは「GET, POST, HEAD」のいずれか
-
設定できるリクエストヘッダは以下のいずれか
- Accept
- Accept-Language
- Content-Language
- Content-Type(条件付き)
-
Content-Typeについては以下のいずれかを満たすこと
- application/x-www-form-urlencoded
- multipart/form-data(ファイルアップロードに使う)
- text/plain(滅多に使わない)
まとめ
-
XSSやCSRFなどの対策として、他のオリジンへのアクセスを制限する「同一オリジンポリシー」という仕組みがある。
-
他のオリジンへアクセスしたい、でも安全性も保ちたい、、を叶えるためCORSが生まれた。
-
通常のCORSはレスポンスが届いたときにレスポンスを受け取るかどうかの制御を行うので、リクエスト自体は無条件に飛んでしまう。そこで不正なリクエストが送られてしまうリスク対策として、プリフライトリクエストが生まれた。
最後に
エラーが解消されて動けばOKではなく、きちんと仕組みを調べてみてかなり理解が深まったかなと思います。(もし誤りがあれば是非ご指摘いただけると幸いです…!!)
これまでHTTPリクエストヘッダやレスポンスヘッダをまじまじと見たことも無かったので、Ajax通信を行っている箇所で検証ツールを確認してみると面白かったです。
確かにOriginやAccess-Control-Allow-Originがあったり、プリフライトリクエストが送られてレスポンスが返って来てから本番のリクエストが送られているのが確認できて、勉強になりました。
セキュリティ対策についてしっかり理解できていなくても動くアプリを作ることが出来てしまうというのは怖いことだなと思うので、これからもセキュリティの勉強はしていきたいと思います。
参考記事