CORS - クロスオリジンAJAX呼び出しのためのSOP補完ポリシー
ブラウザのSOP(Same Origin Policy)ポリシー
ブラウザでのHTTPリクエストは、基本的にSOP(Same Origin Policy)ポリシーに従います。
- オリジンが同一か否かは、以下の3つの要素を基準とします。
- Scheme (http) + Host (1.2.3.4) + Port (8080)
Same Origin Policyという名称から、SOPポリシーがリソースを呼び出したドメインとリソースを提供するドメインが必ず同一でなければならないと誤解されがちですが、セキュリティ上の問題がない範囲で以下のケースについては例外的に許可されます。ちなみに、Cross-site = Cross-domain = Cross-origin はすべて同じ意味です。
- クロスオリジン送信:ユーザーによる意図された送信
<form>タグで他のドメインに**submit()**送信可能
- クロスオリジン取得:単純な参照 = 悪意のあるリクエスト不可
<img>タグで他のドメインの画像ファイルを取得可能<link>タグで他のドメインのCSSを取得可能<script>タグで他のドメインのJavaScriptライブラリを取得可能<iframe>タグも他のドメインのページを取得可能
- クロスオリジンリクエスト(= AJAX):悪意のあるリクエストが可能
- セキュリティの脆弱性により、CORS(Cross-Origin Resource Sharing)ポリシーにのみ適合すれば条件付きで許可
AJAX (Asynchronous JavaScript and XML)
AJAXとは何でしょうか?これは、サーバーと通信する際に使用される非同期JavaScriptをサポートするもので、フロントエンドでデータを取得するためにサーバーを呼び出す際によく使われるaxiosやfetchの根幹となる技術です。現在、ほとんどの主要なウェブブラウザは、サーバーにデータをリクエストするためにXHR(XmlHttpRequest)オブジェクトを内蔵し、非同期処理を行っています。W3C標準ではないため、ブラウザごとに設計方式に違いはありますが、すべてXHRオブジェクトを通じて実装されています。このXHRオブジェクトを通じて、ウェブページがすべてロードされた後でもサーバーにデータをリクエストしたり受け取ったりして、ページの一部だけを更新することができるのです。
- AJAX = 非同期HTTPデータ転送 = 結果を「オブジェクト」として返却
- HTTPデータをバックグラウンドで転送し結果を受け取るが、現在のページには何の影響も与えない
- FORM = 同期HTTPデータ転送 = 結果を「移動する新しいページ」として返却され、レンダリングする
- HTTPデータを転送し、結果ページを受け取ってそのページに移動する
CORSの登場 - クロスオリジンAJAX呼び出しのために
AJAXはどのようなサーバーでも呼び出すことができるため、同一ドメインのサーバーから情報を取得することも、異なるドメインから情報を取得することも可能です。開発者の意図に基づいたAJAX呼び出しのみが可能であれば良いのですが、ユーザーに悪意のあるスクリプトを実行させ、他のドメインサーバーに対して悪意のあるAJAX呼び出しを実行させる**CSRF(Cross-site Request Forgery)**という脆弱性があります。
このような脆弱性があるため、SOPポリシーはAJAXをブロックすべきですが、AJAXはW3C標準ではないにもかかわらず、事実上の非同期標準として使用されているため、CORSという例外ポリシーが導入されました。CORSポリシーは、悪意のあるAJAX呼び出しを防ぐために、クライアント(ブラウザ)とサーバー間で、そのAJAX呼び出しが意図されたものであるかどうかを相互に交差検証する仕組みを提供しています。
CORSポリシーさえ遵守すれば、AJAXを介したクロスオリジン呼び出しを許可するというものです。
CORSポリシー検証手順
CORSはブラウザの実装仕様に含まれるHTTP必須ポリシーであり、SOPと同様にCORSポリシーの通過可否はブラウザが判断します。ブラウザはサーバーから応答を受け取りますが、応答分析後にCORS違反であればそのまま破棄します。したがって、ブラウザを介さずにサーバー間で通信を行う際には、このポリシーは適用されません。
- CORSポリシー通過可否 - 3つの基準ヘッダー
- 許可されたオリジン
- Origin (クライアント)
- Access-Control-Allow-Origin (サーバー)
- 許可されたメソッド
- Access-Control-Request-Method (クライアント)
- Access-Control-Allow-Method (サーバー)
- 許可されたヘッダー
- Access-Control-Request-Headers (クライアント)
- Access-Control-Allow-Headers (サーバー)
- 許可されたオリジン
CORSリクエストの種類
ブラウザはAJAX呼び出しを2種類の組み合わせに分類し、CORSポリシー検証手順を異なる方法で適用しますが、これはドキュメント定義のための区分と見られ、簡単に言えば次のように理解できます。
- いかなるリクエストであっても、(1) 許可されたオリジンの検証は必須です。
- MethodがGET, HEAD, POST(一部Content-type)ではない場合、(2) 許可されたMethodの検証が必要です。
- 非標準のCustom Headerを使用する場合、(3) 許可されたHeaderの検証が必要です。
具体的な学習のために、2種類の組み合わせについて見ていきましょう。
Simple/Preflight Request
AJAXで使用されるHTTPメソッドが、単純な照会に使われるGET、HEADであれば、サーバーを操作できないメソッドであるため、実際のリクエストに対する戻り値を受け取り、それに含まれるヘッダーを通じてCORS検証を行います。しかし、**サーバーの状態を変更するメソッド(POST、PUT、DELETE)**である場合や、カスタムヘッダー(クッキー保存など)が含まれている場合は、実際のリクエストを送信する前に予備リクエスト(Method = OPTIONS)を送り、実際の戻り値なしでヘッダーのみを受け取りCORS検証を行います。CORS検証が完了した後、実際のリクエストを送信してサーバーの状態を変更します。
Simple Request
- メソッド: GET, HEAD, POST(条件付き)
- POST方式の場合、Content-typeは以下の3つのうちのいずれかである必要があります。
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- POST方式の場合、Content-typeは以下の3つのうちのいずれかである必要があります。
- Custom Header: 存在しない場合
サーバーを操作できないメソッドであるため、クライアントは実際のリクエストを送信し、(1) 許可されたオリジンの一致のみを確認します。
- (1) Origin === Access-Control-Allow-Origin
Preflight Request
- メソッド: POST, PUT, DELETE など、またはカスタムヘッダーを伴うGET/HEAD
- Custom Header: 存在する場合
サーバーを操作できるメソッドであるため、クライアントは実際のリクエストではなく予備(プリフライト)リクエストを送信し、(1) 許可されたオリジン、(2) 許可されたメソッド、(3) 許可されたヘッダーのすべての一致を確認します。
- (1) Origin = Access-Control-Allow-Origin
- (2) Access-Control-Request-Method = Access-Control-Allow-Method
- (3) Access-Control-Request-Headers = Access-Control-Allow-Headers
Credential (資格情報)
**資格情報(Credential)**とは、Cookie、Authorization Headers、またはTLSクライアント認証を意味します。資格情報(Credential)は、クライアントがXMLHttpRequest.withCredentialsまたはFetch APIのRequest()コンストラクタのcredentialsオプションを通じて「資格情報モード」を有効にしてリクエストすると、サーバーがクライアントに「資格情報ヘッダー」に値を含めて送信します。この時、サーバーは以下の「CORSヘッダー」を通じて、クライアントが「資格情報ヘッダー」の値を見ることができるか否かを同時に送信し、ブラウザはその「CORSヘッダー」がtrueであればクライアントに「資格情報ヘッダー」を公開し、falseまたは存在しない場合(デフォルトはfalse)は「資格情報ヘッダー」をすべて破棄しクライアントから隠します。
- Access-Control-Allow-Credentials = true
注意すべき点は、Credentialリクエストの場合、CORSヘッダーのAccess-Control-Allow-Originの値が*であってはならないことです。a.comのような具体的なドメインが指定されている必要があります。
例で見てみる
Simple Request
a.comドメインからb.comドメインへAJAX呼び出しを行う場合:
- Methodが(1) GET, HEAD, POST(条件付き)であり、(2) カスタムヘッダーがない場合 = Simple Request
- CORSポリシーはOriginのみを検査
- (1) Origin === Access-Control-Allow-Origin
- CORSポリシーはOriginのみを検査
Preflight Request
a.comドメインからb.comドメインへAJAX呼び出しを行う場合:
- Methodが(1) GET, HEAD, POST(条件付き)であるが、(2) カスタムヘッダーがある場合 = Preflight Request
- Methodが(1) DELETEである場合 = Preflight Request
- CORSポリシーはOrigin/Method/Headersすべてを検査
- (1) Origin = Access-Control-Allow-Origin
- (2) Access-Control-Request-Method = Access-Control-Allow-Method
- (3) Access-Control-Request-Headers = Access-Control-Allow-Headers
- CORSポリシーはOrigin/Method/Headersすべてを検査
追加のCORS関連HTTPレスポンスヘッダー
Access-Control-Allow-MethodsやAccess-Control-Allow-Headersなど、これまで見てきたものとは異なり、サーバーが応答時にさらに送るCORS関連ヘッダーがいくつかあるので、それらを説明して終わりにします。
Access-Control-Max-Age
Preflight Requestの場合、毎回OPTIONS予備リクエストを送受信すると、実際の結果値が返されるまでに時間がかかるため、予備リクエストに対するCORSレスポンスヘッダーの値をブラウザにどのくらいの時間保存できるかをサーバーが指定できます。
Access-Control-Expose-Headers
Access-Control-Allow-HeadersのAllowが、サーバーが**「クライアントがどのヘッダーを送信できるか」を許可するヘッダーであるのに対し、Exposeが付いたこのヘッダーは、「サーバーが送信するヘッダー」の中でブラウザが読み取れるヘッダーを明示**するものです。