Cross-Origin Resource Sharing (CORS)


Cross-Origin Resource Sharing (CORS)

とは何か

Cross-Origin Resource Sharing (CORS)は、他のオリジンへのリクエスト(XMLHttpRequest(XHR), <img> etc.)に対し、「どのオリジンに」「どういうリクエストを」許可するか指定することで、悪意あるサイトへ情報が漏洩するリスクから自サイトとそのユーザを保護できるようにした仕組みのことである。

Cross-Origin Resource Sharing (CORS)は、Webブラウザによって自動的に(一部はXHR/Imageオブジェクトの設定値・プロパティ値に応じて適宜)設定される(アクセス許可を要求するための)リクエストヘッダと、サーバ側が(アクセスを許可する内容としての)応答として返すレスポンスヘッダとで構成される。

Cross-Origin Resource Policy (CORP)との違い

Cross-Origin Resource Sharing (CORS)は、(クロスオリジンリクエストがCross-Origin Resource Policy (CORP)によって制限されていないことを前提に)クロスオリジンリクエストに対して「どのオリジンから」「どのような」リクエストを許可するかを設定するもの。
一方Cross-Origin Resource Policy (CORP)は、もっとそれ以前の「そもそもクロスオリジンリクエストを許可するのかどうか」を制限する。
(つまり、Cross-Origin Resource Policyヘッダにsame-site/same-originが設定されると、クロスオリジンリクエストそのものがまるごと禁止されてる状態。)

プリフライト(preflight)リクエスト

Cross-Origin Resource Sharing (CORS)では、プリフライト(preflight)リクエストと言って、実際のGETPOSTリクエストなどを送信する前に事前リクエストを(OPTIONSメソッドで)送信し、リクエスト先Webサーバに対してCORSの対応状況や許可ポリシーを事前確認するがある。
その事前確認のリクエストのことを「プリフライト(preflight)リクエスト」と呼ぶ。
プリフライトリクエストは、その必要がある場合にWebブラウザによって自動的に行われる。

プリフライトリクエストによって当該リクエストが許可されていない(ex: 自オリジンがAccess-Control-Allow-Originに含まれていない etc.)とわかった場合、Webブラウザは本来のリクエストの発行されないまま、リクエストは失敗する。

例えば以下のようなときに、事前にプリフライトリクエストが発行されている場合がある

  • GET以外のメソッドでリクエストしたとき
  • リクエストヘッダをカスタマイズしてリクエストしたとき

参考

設定方法

サーバサイド

サーバサイドから以下のHTTPレスポンスヘッダを必要に応じて設定する。

Access-Control-Allow-Origin

どのオリジンからのアクセスを許可するかを明示する。

Access-Control-Allow-Origin: <origin> | *

指定可能な値は以下(制限が厳しい(i.e. 安全な)順)

  • <origin>: 許可するオリジン
  • *(ワイルドカード): 「全てのオリジンを許可する」ということ。なお、Cookieなどのクレデンシャル情報を含む場合は使用できない(=ワイルドカードではなく、どのオリジンを許可するかを明示する必要がある)
対となるリクエストヘッダ: Origin

どのオリジンからのリクエストかを表すリクエストヘッダで、Webブラウザによって自動的に付加される。

Origin: <origin>

サーバサイドでAccess-Control-Allow-Originの値をセットする際に、クライアントから送られたOriginヘッダの値を見てセットするような実装を行う場合がある。

Access-Control-Allow-Methods

クロスオリジンリクエストに対し、許可するリクエストメソッドを明示・列挙する。

Access-Control-Allow-Methods: <method>[, <method>]*

指定可能な値は以下(制限が厳しい(i.e. 安全な)順)

  • <method>: 許可するメソッド(コンマ区切り)
  • *(ワイルドカード): 「全てのオリジンを許可する」ということ。なお、Cookieなどのクレデンシャル情報を含む場合は使用できない(=ワイルドカードではなく、どのメソッドを許可するかを明示する必要がある)
対となるリクエストヘッダ: Access-Control-Request-Method

どのメソッドでのリクエストを行うのかを表すリクエストヘッダで、プリフライトリクエスト時にWebブラウザによって自動的に付加される。

Access-Control-Request-Method: <method>
Access-Control-Allow-Headers

クロスオリジンリクエストに対し、許可するリクエストヘッダを明示・列挙する。

Access-Control-Allow-Headers: <header-name>[, <header-name>]*

指定可能な値は以下(制限が厳しい(i.e. 安全な)順)

  • <header-name>: 許可するリクエストヘッダ(コンマ区切り)
  • *(ワイルドカード): 「(Authorizationヘッダを除く)全てのリクエストヘッダを許可する」ということ。なお、Cookieなどのクレデンシャル情報を含む場合は特別な意味のない "*" というヘッダー名として扱われる。また、このワイルドカードを使ってもAuthorizationだけはワイルドカードの対象には含まれず別途明示的に列挙する必要がある。
対となるリクエストヘッダ: Access-Control-Request-Headers

どんなヘッダをリクエストに含めたいのかを表すリクエストヘッダで、プリフライトリクエスト時にWebブラウザによって自動的に付加される。

Origin: <origin>
Access-Control-Expose-Headers

クロスオリジンリクエストを発行したクライアントに対し、ヘッダ情報を開示するレスポンスヘッダを明示・列挙する。
ここに列挙されたレスポンスヘッダは、JavaScript側から値を参照することが出来る。

Access-Control-Expose-Headers: <header-name>[, <header-name>]*

指定可能な値は以下(制限が厳しい(i.e. 安全な)順)

  • <header-name>: 許可するレスポンスヘッダ(コンマ区切り)
  • *(ワイルドカード): 「(Authorizationヘッダを除く)全てのレスポンスヘッダを許可する」ということ。なお、Cookieなどのクレデンシャル情報を含む場合は特別な意味のない "*" というヘッダー名として扱われる。また、このワイルドカードを使ってもAuthorizationだけはワイルドカードの対象には含まれず別途明示的に列挙する必要がある。
CORS セーフリストレスポンスヘッダー

Access-Control-Expose-Headersに明示されていなくても、クライアントからアクセスされても安全とみなされるいくつかのヘッダがある。
これらを「CORS セーフリストレスポンスヘッダー」と呼ぶ。

CORS セーフリストレスポンスヘッダーは以下の通り。

  • Cache-Control
  • Content-Language
  • Content-Length
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma
Access-Control-Allow-Credentials

クロスオリジンリクエストに対し、送信されてきた(Cookieなどの)クレデンシャル情報を使ってレスポンスを生成することを許可しているかを表す。
クロスオリジンリクエストにおいてこのレスポンスヘッダが返ってこなかった場合、Webブラウザは当該リクエストのレスポンスを無視する(JavaScript側からレスポンスが見れなくなる)。

Access-Control-Allow-Credentials: true
Access-Control-Max-Age

クロスオリジンリクエストに対し、プリフライトリクエストのレスポンスのキャッシュ期限を指定する。

Access-Control-Max-Age: <delta-seconds>

<delta-seconds>: キャッシュ期限(秒)

クライアントサイド

クライアントサイドにおいて、CORSを意識する場面として以下がある。

XMLHttpRequestwithCredentialsプロパティ

XMLHttpRequestXHR; いわゆるAjax)において、CookieなどのクレデンシャルをもちいてCORSを行う場合、XMLHttpRequestwithCredentialsプロパティにtrueを設定する必要がある。

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);

これを設定しないと、ブラウザはCORSリクエストにCookieを付加してくれない

参考
Fetch APImode: 'cors'オプションとcredentialsオプション

Fetch APIでCORSリクエストを正常に行うには、modeオプションにcorsを指定する必要がある。
また、それにCookieなどのクレデンシャルを含める場合は、credentialsincludeを指定する。

// Fetch APIでCookieを使ったCORSリクエストを発行
const response = fetch('https://example.com', {
  mode: 'cors',
  credentials: 'include'
});
参考
<img>crossorigin属性、およびImageのcrossOrigin`プロパティ

これを設定することで当該HTML要素が発行するリクエストを、CORSリクエスト化出来る。

Cookieなどのクレデンシャルを含める場合はuse-credentialsを、含めない場合はanonymousを設定する。
なお、空文字や認識できない文字はanonymous扱いされる。

<!-- クレデンシャルを含める場合 -->
<img src="https://example.com" crossorigin="use-credentials">
<script>
  // 上記HTMLと同じ
  const img = new Image()
  img.src = 'https://example.com'
  img.crossOrigin = 'use-credentials'
</script>

<!-- クレデンシャルを含めない場合 -->
<img src="https://example.com" crossorigin>
<img src="https://example.com" crossorigin="">
<img src="https://example.com" crossorigin="anonymous">
<script>
  // 上記HTMLと同じ
  const img = new Image()
  img.src = 'https://example.com'
  img.crossOrigin = 'anonymous'
  // or img.crossOrigin = ''
</script>

なお、Canvasで別オリジンの画像を描画した際の具体的な影響としては、

CORS設定がないとtoDataURL()メソッドなどで画像データを取り出そうとしたときにSecurityErrorで失敗する(i.e. データを取り出さず描画するだけなら問題ない)

などである。

CORS×画像×Canvasで時々起こったりするハマりポイントと対処法

Webブラウザには、過去のリクエストをキャッシュによって描画を高速化する機能がある。そしてキャッシュはその時のレスポンスデータだけでなくどのようなリクエストヘッダ・レスポンスヘッダが送受信されたかも保存されている。
これが時として、「CORSで画像をリクエストしたにもかかわらず、過去にCORSでない方法で取得した画像データがキャッシュによって返される」という事象が起こることがある。
そして前述の通り、CORS上不完全な設定で取得した画像はCanvasでdrawImage()で画像編集するなどのあとtoDataURL()メソッドなどで画像データを取り出そうとしたときにSecurityErrorで失敗する。
これによる不測の不具合発生を防ぐには、画像のリクエストURLにキャッシュバスターを付加するなどでキャッシュを回避する手法が有効。

参考

参照