CORSについての説明と、よくある誤解

ウェブアプリケーションの開発をしていると以下のエラーに遭遇することがあります。

> Access to XMLHttpRequest at 'https://some.com/api/hoge' from origin 'https://another.com/' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
> 

CORS Policyとは何なのか、個人的に理解したことをまとめます。

## CSRFについて
まずCORSに入る前にCSRFの説明です。
CSRF（Cross-Site Request Forgery）は、ウェブの脆弱性の一つで、ユーザーが意図しない操作をウェブサイト上で行わせる攻撃手法です。これは、利用者がログイン状態のウェブサイトに対するリクエストを、利用者に代わって攻撃者が送信することで行われます。これにより、利用者自身の意図しないところでパスワードの変更やデータの更新など、利用者の権限で許可されているあらゆる操作が行われる可能性があります。

[IPA](https://www.ipa.go.jp/security/vuln/websecurity/csrf.html)に掲載されている以下の例が分かりやすいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724126/0db56c2b-88c4-858d-0195-4144ed3701a1.png)

セッション情報は通常利用者のブラウザにCookieとして保存され、次回ウェブアプリケーションアクセス時に自動的にリクエストに付与されます。罠サイトではこれを利用して、利用者が意図しないリクエストをウェブアプリケーションに送り付けることで利用者に不利益な操作を実行します。

## CORSとは

[CORS（Cross-Origin Resource Sharing）](https://fetch.spec.whatwg.org/#http-cors-protocol)は、異なるオリジン間でリソースを共有するための仕様です。ウェブページが自身と異なるオリジンのリソースを安全に取得することを可能にします。

CORS導入前のウェブの世界では[SOP（Same-Origin Policy）](https://datatracker.ietf.org/doc/html/rfc6454)が採用されていました。SOPにおいてはウェブページ中のスクリプトは同一のオリジンにあるリソースのみにアクセス可能で、別オリジンリソースへのアクセスはブラウザにより遮断されていました。

ウェブアプリケーションが複雑さを増す中で、オリジン間でリソースの共有をしたいという需要が高まりました。SOPを一部緩和し、CSRFへのリスクを回避しながらリソース共有を行うための仕組みとして、CORSの仕様化が2005年からスタートしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724126/9efdeac1-cb28-a0e2-54a1-ae4908740261.png)

CORSには単純リクエストとプリフライトリクエストの2種類の仕様があります。

### 単純リクエスト
単純リクエストの対象となるのはGET、HEAD、POSTメソッドで、一部のHeaderやContent-Typeが指定さたシンプルなリクエストのみです。
詳細な仕様説明は[MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#%E5%8D%98%E7%B4%94%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88)に譲ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724126/0a4fc5cc-0e64-2a1a-293d-9f1348e5dc89.png)

ブラウザからのリクエストHeaderにはOriginの項目が付与されます。
サーバ側ではリソース共有を許可する対象を、Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methodsレスポンスヘッダに指定します。これらのヘッダには”\*”または特定の値が入ります。”\*”が指定された場合にはすべてのOrigin, Header, Methodを受け入れることになり、リソースの保護がされずセキュリティ的に弱くなります。プロダクションにおいては具体的な値を指定することが求められます。

ブラウザはこのレスポンスを受け、Access-Control-Allow-XXXの内容がリクエストと一致する場合のみレスポンスを受け入れます。一致しない場合はエラーを返します。

重要なのは、クロスオリジンを判定するのはブラウザであり、サーバ側ではないということです。サーバ側はクロスオリジンのリクエストであってもそれに応じた処理を実行します。

### プリフライトリクエスト
単純リクエスト以外のクロスオリジンリクエストはプリフライトリクエストとして扱われます。プリフライトリクエストでは、メインリクエストを送信する前にOPTIONSメソッドを使ったプリフライトをサーバに送信し、その後メインリクエストを送信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724126/d6b57396-7a95-40d8-56ec-8e6bca6bb057.png)

なぜプリフライトリクエストのような仕組みが必要なのでしょうか？
単純リクエストでも述べたように、CORSの仕様ではクロスオリジン判定を行うのはブラウザ側であり、サーバサイドでは処理が実行されます。GETやHEADのような副作用のない操作であれば単純リクエストのようにいきなりメインリクエストを送ることに問題はないのですが、副作用を伴う操作を行う場合にはメインリクエストを送ってしまうことを防がなければなりません。プリフライトはメインリクエストを送る前に問題ないリクエストであるかをブラウザ側で判定するために必要となります。
CORSに非対応のレガシーなサーバでは、プリフライトに対するレスポンスにAccess-Control-Allow-Originなどのヘッダが存在しないので、メインリクエストが送信されることなくリクエストは失敗します。結果として、SOPと同様の挙動になります。

### なぜ単純リクエストにPOSTが対象となっているのか？
プリフライトの説明を読んでなぜ単純リクエストでPOSTが対象になっているのかと思った方もいるかと思います。
これはCORS以前に決定されたHTML4.0のform仕様に関係しています。formによるPOSTの呼び出しにはSOPが適用されず、クロスオリジンなリクエストであっても通ってしまいます。Cookieも付与されるため、formの仕様はCSRFの脆弱性を抱えていることになります。
~~formによるPOSTにもCORS Policyを適用すればリスクが低減されますが、後方互換性が壊れ既存のサイトが動作しなくなるためこれは採用できません。既知のリスクに対しては別途サーバ側が対策していることを前提としてCORSの仕様は決定されています。~~
※ご指摘を受け上記を訂正します。

サーバサイドがformの脆弱性に対応済みであるという前提に立った場合、わざわざプリフライトを送らなくてもCSRFの危険性は回避できていることになります。そのためシンプルなPOSTについては単純リクエストを採用しています。

## CORSとCookie
CSRFはCookieに保存されたセッション情報を利用する攻撃なので、CORSにおいてCookieの利用を制限することは重要です。CORS仕様ではデフォルトでクロスサイトのリクエストにCookieを付与しません。Cookieを付与するにはクライアント側でXMLHttpRequestのwithCredentialsフラグをtrueにし、サーバ側でAccess-Control-Allow-Credentials: trueヘッダーを付与する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3724126/94352d83-ab9f-99e0-bf9f-3450958f542a.png)

プリフライトリクエストの場合にはプリフライトのレスポンスにAccess-Control-Allow-Credentials: trueヘッダーを付与する必要があります。これが無い場合、ブラウザ側で以下のエラーが出力されレスポンスが無視されます。
> Access to XMLHttpRequest at 'https://some.com/api/hoge' from origin 'https://another.com/' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

また、Cookieを付与する場合はAccess-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methodsの各項目で、"\*"を指定することができなくなります。必ず特定の値を指定する必要があります。

## CORSにまつわるよくある誤解

### CORSとはセキュリティを強化するための仕様である
間違い。従来採用されていたSOPのほうがセキュリティ的には厳しいです。CORSはSOPを緩和するための仕様です。
CORSによって従来のSOPを前提に作られたサーバアプリケーションを危険にさらさないように、このような複雑な仕様になっています。

### CORS仕様ではクロスオリジンのリクエストはサーバサイドでブロックされる
間違い。CORSの仕様ではリクエストをブロックするのはブラウザ（クライアント）側です。サーバサイドはAccess-Control-Allow-XXXのHeaderを付与する必要がありますが、クロスオリジンのリクエストに対して通常と同様に応答します。

### CORSによって外部サイトからのformによるリクエストもブロックされる
間違い。[なぜ単純リクエストにPOSTが対象となっているのか？](なぜ単純リクエストにPOSTが対象となっているのか？)参照。

### CORSによってcurlによるリクエストはブロックされる
間違い。CORSはブラウザ側でブロックするための仕組みなので、curlのようなツールには関係ありません。

### CORSの仕様は全てのメソッドで同一である
間違い。先にも書いたように単純リクエストであるかプリフライトリクエストであるかはメソッドと一部ヘッダ情報により判断されます。
単純リクエストではサーバ側での処理は実行されるので、例えばGETリクエストで副作用のある処理を行うことはCSRFに対する脆弱性を埋め込む危険性があります。
[W3CのRecommendation](https://www.w3.org/TR/2014/REC-cors-20140116/#terminology)には以下のように記載されています。
> Resource authors are strongly encouraged to ensure that requests using safe methods, e.g. GET or OPTIONS, have no side effects so potential attackers cannot modify the user's data easily. If resources are set up like this, attackers would effectively have to be on the list of origins to do harm.

> リソース作成者は、安全なメソッド、例えばGETやOPTIONSを使ったリクエストに副作用がないことを保証し、潜在的な攻撃者がユーザーのデータを簡単に変更できないようにすることを強く推奨します。リソースがこのように設定されている場合、攻撃者は損害を与えるために、事実上、発信元リストに載っていなければならなくなります。

### CORSによってCSRFからは完全に守られる
間違い。CORSはCSRFへの部分的な対策に過ぎません。より根本的な対策については[IPAの安全なウェブサイトの作り方 - 1.6 CSRF](https://www.ipa.go.jp/security/vuln/websecurity/csrf.html)を参照してください。
またCookieには[SameSite属性](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7)を指定することができ、異なるドメインからのリクエストへのCookieの付与を制限できます。こちらはCSRF含めたCookieを利用した様々な攻撃に対する防御策となります。

### まとめ
CORSの仕様は後方互換性を保ちながら脆弱とならないよう上手く考えられていると感じました。ただその分複雑で、背景にどのような事情があるのか理解するのが困難です。
本稿が理解のための一助となれば幸いです。