CORS (cross origin resource sharing)

  • オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み
  • XMLHttpRequestや Fetch API は同一オリジンポリシーsame-origin policyに従う
  • XMLHttpRequestや Fetch API 使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンの場合は正しい CORS ヘッダーを含んでいることが必要
  • オリジン間共有仕様は、以下のようなサイト間 HTTP リクエストを有効にする
    • XMLHttpRequest または Fetch API を呼び出す
    • CSS の @font-face で別ドメインのフォントを利用
    • WebGL テクスチャ
    • drawImage() (en-US) を使用してキャンバスに描画される画像やビデオフレーム
    • 画像から生成する CSS シェイプ
  • オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。
  • サーバーの情報に副作用を引き起こすことがある HTTP のリクエストメソッド (特に GET 以外の HTTP メソッドや、特定の MIME タイプを伴う POST) のために、ブラウザーが HTTP の OPTIONS リクエストメソッドを用いて、あらかじめリクエストの「プリフライト」 (サーバーから対応するメソッドの一覧を収集すること) を行い、サーバーの「認可」のもとに実際のリクエストを送信することを指示
  • サーバーはリクエスト時に「資格情報」 (Cookie や HTTP 認証 など) を送信するべきかをクライアントに伝えることもできる
  • CORS は様々なエラーで失敗することがありますが、セキュリティ上の理由から、エラーについて JavaScript から知ることができないよう定められている
  • 何が悪かったのかを具体的に知ることができる唯一の方法は、ブラウザーのコンソールで詳細を見ること
  • request type
    • Simple requests
    • Preflighted requests
    • Requests with credentials

単純リクエス

  • リクエストによっては CORS プリフライトを引き起こさないものがあり、これを「単純リクエスト」と呼ぶ
  • 「単純リクエスト」は、以下のすべての条件を満たすものです。
    • 許可されているメソッドのうちの一つであること。
      • 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 / これは仕様の一部ではないので、他のブラウザーはこの追加の制限を実装していません。

// 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-Max-Age
    • プリフライトリクエストの結果をキャッシュしてよい時間
    • 秒単位
  • Access-Control-Allow-Credentials
    • credentials フラグが真であるときに、リクエストへのレスポンスを開示してよいか否かを示す
    • プリフライトリクエストのレスポンスの一部として用いられたときは、実際のリクエストで資格情報を使用してよいか否かを示す
    • 単純な GET リクエストはプリフライトを行いませんので、リソースへのリクエストが資格情報付きで行われた場合にリソースと共にこのヘッダーを返さなければ、レスポンスはブラウザーによって無視され、ウェブコンテンツに返らない
  • Access-Control-Allow-Methods
    • リソースへのアクセス時に許可するメソッドを指定
    • プリフライトリクエストのレスポンスで用いる
  • Access-Control-Allow-Headers
    • 実際のリクエストでどの HTTP ヘッダーを使用できるかを示すために、プリフライトリクエストのレスポンスで使用

HTTP リクエストヘッダー

  • サイト間 XMLHttpRequest の機能を使用する開発者は、オリジン間リソース共有のヘッダーをプログラムで設定する必要なし
  • Origin
    • サイト間のアクセスリクエストやプリフライトリクエストのオリジンを示します。
    • パス情報は含めず、サーバー名だけにします。
    • すべてのアクセス制御リクエストにおいて、 Origin ヘッダーは常に送信
  • Access-Control-Request-Method
    • プリフライトリクエストを発信する際に、実際のリクエストを行う際に使用する HTTP メソッドをサーバーに知らせるために使用
  • Access-Control-Request-Headers
    • プリフライトリクエストを発信する際に、実際のリクエストを行う際に使用する HTTP ヘッダーをサーバーに知らせるために使用