CORS (cross origin resource sharing)
- オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み
- ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行
- https://domain-a.com で提供されているウェブアプリケーションのフロントエンド JavaScript コードが XMLHttpRequest を使用して https://domain-b.com/data.json へリクエストを行う場合。セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限
- XMLHttpRequestや Fetch API は同一オリジンポリシーsame-origin policyに従う
- XMLHttpRequestや Fetch API 使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンの場合は正しい CORS ヘッダーを含んでいることが必要
- オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。
- サーバーの情報に副作用を引き起こすことがある HTTP のリクエストメソッド (特に GET 以外の HTTP メソッドや、特定の MIME タイプを伴う POST) のために、ブラウザーが HTTP の OPTIONS リクエストメソッドを用いて、あらかじめリクエストの「プリフライト」 (サーバーから対応するメソッドの一覧を収集すること) を行い、サーバーの「認可」のもとに実際のリクエストを送信することを指示
- CORS は様々なエラーで失敗することがありますが、セキュリティ上の理由から、エラーについて JavaScript から知ることができないよう定められている
- 何が悪かったのかを具体的に知ることができる唯一の方法は、ブラウザーのコンソールで詳細を見ること
- request type
- Simple requests
- Preflighted requests
- Requests with credentials
単純リクエスト
- 「単純リクエスト」は、以下のすべての条件を満たすものです。
- 許可されているメソッドのうちの一つであること。
- GET
- HEAD
- POST
- ユーザーエージェントによって自動的に設定されたヘッダー (たとえば Connection、 User-Agent、 または Fetch 仕様書で「禁止ヘッダー名」として定義されているヘッダー) を除いて、手動で設定できるヘッダーは、 Fetch 仕様書で「CORS セーフリストリクエストヘッダー」として定義されている以下のヘッダーだけ
- Accept
- Accept-Language
- Content-Language
- Content-Type (但し、下記の要件を満たすもの)
- DPR
- Downlink (en-US)
- Save-Data
- Viewport-Width
- Width
- Content-Type ヘッダーでは以下の値のみが許可されています。
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
- リクエストに使用されるどの XMLHttpRequestUpload にもイベントリスナーが登録されていないこと。これらは正しく XMLHttpRequest.upload を使用してアクセスされます。
- リクエストに ReadableStream オブジェクトが使用されていないこと。
- 許可されているメソッドのうちの一つであること。
- 注: これらはウェブコンテンツが発行可能になっているサイト間リクエストと同じ種類のものであり、サーバーが適切なヘッダーを送信しなければレスポンスデータは送信元へ送られません。従ってクロスサイトリクエストフォージェリ対策をしているサイトは、 HTTP アクセス制限について新たに心配することはありません。
- 注: WebKit Nightly および Safari Technology Preview は、 Accept, Accept-Language, Content-Language ヘッダーの値に追加の制限を掛けています。これらのヘッダーが「標準外」の値の場合、 WebKit/Safari はそのリクエストが「単純リクエスト」の条件に合うとは判断しません。 WebKit/Safari がこれらのヘッダーのどの値を「標準外」と判断するかについては、以下の WebKit のバグを除いて文書化されていません。Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language / Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS / Switch to a blacklist model for restricted Accept headers in simple CORS requests / これは仕様の一部ではないので、他のブラウザーはこの追加の制限を実装していません。
- https://foo.example にあるウェブコンテンツがドメイン https://bar.other にあるコンテンツを呼び出したいと仮定
// src const xhr = new XMLHttpRequest(); const url = 'https://bar.other/resources/public-data/'; xhr.open('GET', url); xhr.onreadystatechange = someHandler; xhr.send();
bar.otherへの リクエスト GET /resources/public-data/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://foo.example
特筆すべきリクエストヘッダーは Origin であり、呼び出しが https://foo.example から来たことを表す
bar.otherからのレスポンス HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 00:23:53 GMT Server: Apache/2 Access-Control-Allow-Origin: * Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/xml
レスポンスでは、サーバーが Access-Control-Allow-Origin ヘッダーを返信
この場合、サーバーは Access-Control-Allow-Origin: * を返しており、これはそのリソースがすべてのドメインからアクセスできることを意味
https://bar.other にあるリソースの所有者が、リソースへの制限を https://foo.example からのリクエストのみに制限したい場合
Access-Control-Allow-Origin: https://foo.example
Origin ヘッダーと Access-Control-Allow-Origin ヘッダーの使用は、最も単純なアクセス制御プロトコルを表す
プリフライトリクエスト
- 始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめる
- サイト間リクエストがユーザーデータに影響を与える可能性があるような場合に、このようにプリフライトを行う
// exsample const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://bar.other/resources/post-here/'); xhr.setRequestHeader('X-PINGOTHER', 'pingpong'); xhr.setRequestHeader('Content-Type', 'application/xml'); xhr.onreadystatechange = handler; xhr.send('<person><name>Arun</name></person>');
POST で送信する XML の本体を作成
標準外の X-PINGOTHER HTTP リクエストヘッダーを設定
このようなヘッダーは HTTP/1.1 プロトコルに含まれていませんが、ウェブアプリケーションでは一般的に便利
Content-Type に application/xml を使用し、かつカスタムヘッダーを設定しているため、このリクエストではプリフライトを行う
注: 後述するように、実際の POST リクエストには Access-Control-Request-* ヘッダーが含まれず、 OPTIONS リクエストのみで必要になります。
// プリフライトリクエスト OPTIONS /doc HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: http://foo.example Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type // プリフライトレスポンス HTTP/1.1 204 No Content Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Keep-Alive: timeout=2, max=100 Connection: Keep-Alive
・リクエスト解説
ブラウザーは上記で使用された JavaScript コードで使用しているリクエストの引数に基づいて、プリフライトの送信が必要であることを判断
これによりサーバーは実際のリクエストの引数によって、送られるリクエストが受け入れ可能かをレスポンスできる
OPTIONS はサーバーから付加的な情報を得るために用いる HTTP/1.1 のメソッドであり、また安全なメソッド、つまりリソースを変更するためには使用できないメソッド
Access-Control-Request-Method ヘッダーは、プリフライトリクエストの一部として、実際のリクエストが POST リクエストメソッドで送られることをサーバーに通知
Request-Headers ヘッダーは、実際のリクエストにカスタムヘッダーである X-PINGOTHER および Content-Type が含まれることをサーバーに通知
ここでサーバーは、この状況下でリクエストの受け入れるかを判断する機会がある
・レスポンス解説
Access-Control-Allow-Methods は当該リソースへの問い合わせに POST および GET が実行可能であることを伝える
Access-Control-Allow-Methods ヘッダーはレスポンスヘッダーの Allow と似ていますが、アクセス制御でのみ使用
サーバーは、 Access-Control-Allow-Headers を X-PINGOTHER の値で送信し、これが実際のリクエストで使用されるヘッダーであることを承認
Access-Control-Allow-Headers は受け入れ可能なヘッダーをカンマ区切りのリストで表す
Access-Control-Max-Age は、プリフライトリクエストを再び送らなくてもいいように、プリフライトのレスポンスをキャッシュしてよい時間を秒数で与える
この例では86400秒、つまり24時間
ブラウザーは個々に内部の上限値を持っており、 Access-Control-Max-Age が上回った場合に制限を掛ける
// 本命のリクエスト POST /doc HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive X-PINGOTHER: pingpong Content-Type: text/xml; charset=UTF-8 Referer: https://foo.example/examples/preflightInvocation.html Content-Length: 55 Origin: https://foo.example Pragma: no-cache Cache-Control: no-cache <person><name>Arun</name></person> // 本命のレスポンス HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:40 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 235 Keep-Alive: timeout=2, max=99 Connection: Keep-Alive Content-Type: text/plain [Some XML payload]
プリフライトリクエストとリダイレクト
"The request was redirected to 'https://example.com/foo', which is disallowed for cross-origin requests that require preflight"
"Request requires preflight, which is disallowed to follow cross-origin redirect"
- もともと CORS プロトコルはそのような振る舞いを要求してたが、その後で必要なしに変更されたが、多くのブラウザーはまだ変更を実装しておらず、もともと要求されていた振る舞いに従っている
- ブラウザーが仕様に追いつくまで、以下の一方もしくは両方を行うことでこの制限を回避できる
- これらの変更ができない場合は、次のような別な方法があります。
- 単純リクエストを行い (Fetch API の Response.url (en-US) または XMLHttpRequest.responseURL を使用して)、実際のプリフライトリクエストが転送される先を特定する。
- 最初のステップの Response.url または XMLHttpRequest.responseURL で得た URL を使用して、もう一つのリクエスト (「本当の」リクエスト) を行う。
- リクエストに Authorization ヘッダーが存在するためにプリフライトを引き起こすリクエストの場合、上記の手順を使用して制限を回避できない
資格情報を含むリクエスト
- XMLHttpRequest や Fetch と CORS の両方が、 HTTP クッキーと HTTP 資格情報によってわかる「資格情報を含む」リクエストを作成することができる
- 既定では、サイト間の XMLHttpRequest または Fetch の呼び出しにおいて、ブラウザーは資格情報を送信しない
- XMLHttpRequest オブジェクトまたは Request のコンストラクターの呼び出し時に、特定のフラグの設定が必要
- http://foo.example から読み込まれた元のコンテンツが、 http://bar.other にあるリソースに対してクッキーを設定したシンプルな GET リクエスト
const invocation = new XMLHttpRequest(); const url = 'http://bar.other/resources/credentialed-content/'; function callOtherDomain() { if (invocation) { invocation.open('GET', url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); } }
XMLHttpRequest に設定する必要があるフラグ、 withCredentials という真偽値型の値を設定
既定では、クッキーなしで呼び出す
単純な GET リクエストなのでプリフライトは行いませんが、ブラウザーは Access-Control-Allow-Credentials: true ヘッダーを持たないレスポンスを拒否し、ウェブコンテンツを呼び出すレスポンスを作成しない
// request GET /resources/credentialed-content/ HTTP/1.1 Host: bar.other User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Referer: http://foo.example/examples/credential.html Origin: http://foo.example Cookie: pageAccess=2 // response HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:34:52 GMT Server: Apache/2 Access-Control-Allow-Origin: https://foo.example Access-Control-Allow-Credentials: true Cache-Control: no-cache Pragma: no-cache Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT Vary: Accept-Encoding, Origin Content-Encoding: gzip Content-Length: 106 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain [text/plain payload]
http://bar.other 向けのクッキーが含まれていますが、bar.other が Access-Control-Allow-Credentials: true (17行目) をレスポンスに含めなければ、レスポンスは無視されウェブコンテンツで使用できない
資格情報付きリクエストとワイルドカード
- 格情報を含むリクエストに対するレスポンスの時、サーバーは Access-Control-Allow-Origin ヘッダーで "*" ワイルドカードではなくオリジンを指定しなければならない
- 上記のリクエストヘッダーは Cookie ヘッダーを含んでいるため、 Access-Control-Allow-Origin ヘッダーが "*" であったらリクエストは失敗
- Access-Control-Allow-Origin ヘッダーの値が "*" ワイルドカードではなく "http://foo.example" (実際のオリジン) なので、ウェブコンテンツの呼び出しに対して資格情報を意識したコンテンツが返る
- 上記の中にある Set-Cookie レスポンスヘッダーは、将来のクッキーの設定も行ないます。失敗した場合、 (使われている API によりますが) 例外が発生
- CORS のレスポンスに設定されたクッキーは、サードパーティーのクッキーに関する通常のポリシーに従う
- 上記では、ページは foo.example から読み込まれてるが、クッキーは bar.other から送られているので、ユーザーがブラウザーでサードパーティーのクッキーをすべて拒否するよう設定していた場合は保存されない
HTTP レスポンスヘッダー
- Access-Control-Allow-Origin
- Access-Control-Allow-Origin は、リソースへのアクセスを許可するオリジンをブラウザーに伝えるための単一のオリジンを指定することができます。
- 資格情報を含まないリクエストのみ、どのオリジンにもリソースへのアクセスを許可することをブラウザーに伝えるワイルドカード "*" を指定することができます。
- サーバーがワイルドカード "*" ではなく (ホワイトリストの一部としてリクエストするオリジンに基づいて動的に変更される可能性がある) 単一のオリジンを指定した場合は、サーバーは Vary レスポンスヘッダーに Origin も含めて、サーバーのレスポンスが Origin リクエストヘッダーの値によって変化することをクライアントに知らせる必要がある
Access-Control-Allow-Origin: https://mozilla.org
Vary: Origin
- Access-Control-Allow-Credentials