はじめに
先日、CORSについて質問を受けたのですが、うまく答えられませんでした。「なんとなくわかっている」状態だったことに気づき、きっちり学び直すことにしました。
この記事では、CORSを学ぶ中で自分が感じた疑問とその答えを、Q&A形式でまとめています。同じように「なんとなく」で止まっている方の参考になれば幸いです。
CORSとは何か
CORSとは、ブラウザが、異なるオリジンへのHTTPリクエストの際に、レスポンスをJavaScriptに返すかどうかを制御する仕組みです。
サーバーからのレスポンスのAccess-Control-Allow-Originヘッダーで許可されていればJavaScriptにレスポンスを渡し、許可されていなければ渡しません。
ここでいう「オリジン」とは、プロトコル・ホスト・ポートの組み合わせのことです。たとえば https://example.com:443 が一つのオリジンです。
疑問1: なぜサーバー側が設定するのか?
CORSの設定は、アクセスされるサーバー側で行います。
「アクセス元で制御すればいいのでは?」
最初に思ったのがこれでした。意図せぬ別オリジンへのアクセスを防ぎたいなら、アクセス元のオリジン側で「アクセスしていい先」を設定すればいいのではないか、と。
しかし、これではセキュリティとして機能しません。なぜなら、攻撃者はアクセス元を自由にコントロールできるからです。
具体例で考えてみます。
- ユーザーが
bank.comにログイン済みの状態で、悪意あるサイトevil.comを開く -
evil.comのJavaScriptがbank.com/api/accountにリクエストを送る - ブラウザはユーザーの
bank.com向けCookieを自動的に付与する
もし「アクセス元側で許可先を設定する」仕組みだった場合、evil.comの開発者が自分の設定に「bank.comへのアクセスを許可」と書くだけで、ユーザーのCookieを使ってbank.comのデータを取り放題になってしまいます。
一方、bank.com側が「evil.comからのリクエストは許可しない」と宣言する仕組みであれば、攻撃者はbank.comのサーバー設定を変えられないので、防御が成り立ちます。
守るべきデータを持っているのはサーバー側なので、サーバー側が決定権を持つという設計です。
疑問2: なぜリクエスト時ではなくレスポンス時に制御するのか?
CORSは、リクエストの送信自体をブロックするのではなく、レスポンスをJavaScriptに渡すかどうかを制御しています。
「リクエスト時点でリクエスト先のオリジンもわかっているのだから、そこでブロックすればいいのでは?」と思いましたが、これには歴史的な理由があります。
CORSが策定される前から、HTMLの<form>タグによるPOST送信や<img>タグによるGETリクエストは、別オリジンに対して普通に行われていました。これらを急にブロックすると、既存のWebサイトが壊れてしまいます。
重要なのは、これらのHTMLタグによるリクエストはCORSの対象ではないという点です。
-
<form>によるPOST送信 → ページ遷移が発生し、レスポンスは新しいページとして表示されるだけ。元のページのJavaScriptはレスポンスを読み取れない -
<img>によるGET → ブラウザが画像を表示するが、JavaScriptからピクセルデータを読み取ることはできない(canvasで読み取ろうとするとブロックされる)
CORSが制御するのは、fetchやXMLHttpRequestによるJavaScriptからのプログラム的なアクセスです。
つまり、同一オリジンポリシーが守っているのはレスポンスの読み取りであり、リクエストの送信自体を防ぐことが主目的ではない、という設計です。
補足: プリフライトリクエスト
ただし、すべてのリクエストがそのまま送信されるわけではありません。
単純なGETやPOST以外のリクエスト(PUT、DELETE、カスタムヘッダー付きなど)では、ブラウザが本番リクエストの前にOPTIONSメソッドでプリフライトリクエストを送ります。プリフライトのレスポンスのAccess-Control-Allow-Originヘッダーにリクエスト元のオリジンが含まれていなければ、その段階でCORSエラーになり、本番リクエストは送信されません。
これらは歴史的にHTMLタグでは送れなかったタイプのリクエストなので、互換性を気にせずリクエスト段階でブロックできるわけです。
疑問3: レスポンスが返ってきているなら読み取れるのでは?
「JavaScriptに渡されなくても、レスポンス自体は返ってきているなら読み取ろうと思えばできてしまうのでは?」と思いました。
結論から言うと、読み取れません。
CORSはブラウザに実装された仕組みであり、レスポンスのブロックはブラウザのネットワーク層で行われます。JavaScriptからはレスポンスの存在自体が見えない状態になります。ステータスコードもヘッダーもボディも、JavaScriptのAPIからは一切アクセスできません。
「ブラウザの開発者ツールのNetworkタブでは見えるのでは?」と思うかもしれませんが、それは開発者ツールがブラウザの内部に特権的にアクセスしているからです。ページ上で動くJavaScriptにはその権限がありません。
疑問4: プロキシなら読めるのでは?
「ブラウザのネットワーク層でブロックされるなら、通信経路上にプロキシを挟めば読めるのでは?」と考えました。
理論的にはその通りで、CORSはあくまでブラウザ内のJavaScriptからのアクセスを制御する仕組みです。curlやPostman、サーバーサイドのコードなど、ブラウザを介さない手段ではCORSは一切関係ありません。
では、ユーザーのブラウザとサーバーの間にプロキシを挟んで中間者攻撃(MITM)すれば読めるのでは?と思いましたが、ここはHTTPSが防いでくれます。通信が暗号化されているため、間にプロキシを挟んでも内容を読み取れません。
つまり、CORSとHTTPSは別のレイヤーの防御であり、組み合わせることでセキュリティが成り立っています。
- CORS: 悪意あるJavaScriptによるレスポンス読み取りを防ぐ
- HTTPS: 通信経路上での盗聴を防ぐ
疑問5: CORSだけでサーバーは守れるのか?
ここまで学んで一つ気になったのが、「単純なリクエストはサーバーに届いて処理されている」という点です。
CORSはレスポンスの読み取りを防ぎますが、リクエストの送信(=サーバーへの副作用)は防げません。たとえば、evil.comからbank.com/transferへのPOSTリクエストは、CORSに関係なくサーバーに届いて処理される可能性があります。
これがCSRF(クロスサイトリクエストフォージェリ)攻撃であり、CORSだけでは防げない領域です。そのためサーバー側でCSRFトークンなど別の対策が必要になります。
まとめ
学び直した結果をまとめます。
-
CORSとは: ブラウザが、異なるオリジンへのHTTPリクエストのレスポンスをJavaScriptに渡すかどうかを制御する仕組み。サーバーの
Access-Control-Allow-Originヘッダーに基づいて判定する。 - 設定がサーバー側である理由: 守るべきデータを持っているのがサーバー側だから。アクセス元側で制御しても、攻撃者自身が設定を変えられるため意味がない。
-
レスポンス時に制御する理由: CORS登場前からHTMLの
<form>や<img>による別オリジンへのリクエストは普通に行われており、これらをブロックすると既存のWebサイトが壊れるため。CORSはJavaScriptによるレスポンスの読み取りだけを制御している。