Written by TSUYOSHI

【CORS】JavaScriptにおけるCORSやPreflightを理解する

JavaScript PROGRAMMING

本記事では、初心者向けにJavaScriptでAjaxによるAPI通信などで同一オリジンによる制限や、CORS・preflightとは何かについて解説します。

この記事を書いている僕は、フロントエンドのプログラマー(フリーランス)として、これまで4年ほどのWeb制作経験があり、Vue,React,Nuxtなどフロントエンドの開発を得意としています。

この記事を読むことによって、CORSや同一オリジンポリシー・クロスオリジン、preflightとは何なのかが分かるようになります。

【CORS】JavaScriptにおけるCORSやPreflightを理解する

まずCORSの説明については、徳丸先生のYouTube動画が超わかりやすくて参考になります。

悪意のあるサイトからJavaScriptを使っての攻撃を防ぐ目的でブラウザには「同一オリジンポリシー」があり、違うドメインなどのクロスオリジンへのアクセスは制限があります。しかし、CORSの機能提供によって、クロスオリジンへのリクエストが可能となります。

以下のようなサイトでは、JavaScriptで異なるドメインのAPIを呼び出すことがあると思います。

・VueやReactなどのシングルページアプリケーション開発
・JavaScriptをよく使いAjax通信をするような開発

CORSを理解していないと同一オリジンポリシーの関係でリソースを正しく取得できなかったりします。ここではJavaScriptでクロスドメインで通信するためのCORSについて解説していきます。

CORSを理解するために、事前知識として、「同一オリジンポリシー」について解説します。

「同一オリジン」とは?

オリジンは「スキーム」「ホスト」「ポート」の組み合わせのことです。

例えば「http://example.com」の場合であれば、以下のようになります。

スキーム → http
ホスト → example.com
ポート → 80番ポート (httpのデフォルト値)

http通信では、「プロトコル + ホスト + ポート」がすべて同じなら同一オリジンとなります。

そして、「同一オリジンポリシー」は「あるオリジンから読み込まれた文書やスクリプトについて、そのリソースから他のオリジンのリソースにアクセスできないように制限するもの」です。

例えばexample.comのサイトにアクセスして、そのサイトから取得したJavaScriptapi.example.comのサイトにAjaxでリクエストをしようとしても、クロスオリジン(同一オリジンではない)ということでリクエストがブロックされるという仕組みです。これによって悪意のあるサイトからのJavaScriptでの攻撃を防ぐことができ、ブラウザに実装されています。

ただし、これだとJavaScriptAjaxで異なるオリジンのAPIを呼び出すということ等ができなかったりするため、「CORS」という機能が提供されるようになりました。(JSONPという方法もありましたがJSONPは裏技のようなもので、安全性の問題から今は基本的には使われないと思います。)

CORS(Cross-Origin Resource Sharing)の特徴

CORSによって、同一オリジンではない「クロスオリジン」へのアクセスが制御により可能になりました。現在はJavaScriptからクロスオリジンのAPIへリクエストをしてデータを取得するということは当たり前になってきているのでほぼ必須の機能です。

CORSのリクエストヘッダー・レスポンスヘッダー

HTTPヘッダーを用いたアクセス制御では、リクエストとレスポンスのヘッダーに以下のような情報を入れて、判断をします。

リクエストヘッダー:「Origin」,「Access-Control-Request-xxxx
レスポンスヘッダー:「Access-Control-Allow-xxxx

単純リクエスト → 無条件にリクエストを送信でき、レスポンスヘッダーによって、JavaScriptがリソースを受け取れるかを判断する。
単純リクエスト以外プリフライトリクエスト(Preflight)で事前にリクエスト送信の許可を確認して、その後にリクエストを送信する。

単純リクエストの要件

「単純リクエスト」は以下のすべての条件を満たすものになります。

▼メソッドが以下のいずれか
GET
POST
HEAD
 
▼設定できるリクエストヘッダ
Accept
Accept-Language
Content-Language
Content-Type (但し、下記の要件を満たすもの)
DPR
Downlink
Save-Data
Viewport-Width
Width
 
※Content-Type ヘッダーでは以下の値のみが許可されている
application/x-www-form-urlencoded
multipart/form-data
text/plain

単純リクエストによる通信の例

ドメイン https://foo.example にあるウェブコンテンツが、ドメイン https://bar.other にあるコンテンツを呼び出したい場合の例です。

const xhr = new XMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

ユーザーがブラウザで https://foo.example へアクセスすると以下のような通信が行われます。

▼ブラウザからサーバーへリクエストするヘッダー内容

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」というように、「https://foo.example」からのアクセスであることをサーバーへ伝えています。

▼サーバーからブラウザへレスポンスするヘッダー内容

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

[…XML データ…]

Access-Control-Allow-Origin: *」で「*」はすべてのオリジンからのアクセスを許可していることがわかります(サーバー側の設定にて)。
この情報によって、ブラウザ側ではこのレスポンスの情報を受取ることができます。ちなみに「https://foo.example」からのみにアクセスを制限する場合は、サーバー側で「Access-Control-Allow-Origin: https://foo.example」のように「Access-Control-Allow-Origin」を制限する必要があります

単純リクエスト以外で、プリフライトリクエスト(preflight request)を使う通信の例

単純リクエスト以外のリクエストでは、最初にプリフライトリクエストを送ってサーバー側に事前に確認をした上で、OKであれば次に本リクエストを送信し、NGであれば本リクエストは送られません。

ドメイン https://foo.example にあるウェブコンテンツが、ドメイン https://bar.other/resources/post-here/POSTカスタムヘッダーを付けて送信する場合の例です。

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('Arun');

Content-Type」 に 「application/xml」を使用してカスタムヘッダーを設定していることによって、プリフライトリクエストが飛ぶようになっています。

以下のように通信は、プリフライトリクエスト(Preflight request)の後に、本リクエスト(Main request)を送っているのがわかります。

▼プリフライトリクエスト(ブラウザ→サーバー)とレスポンス(サーバー→ブラウザ)の例

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

プリフライトリクエストでは、「Origin」と「Access-Control-Request-Method」を送って、POSTでのアクセス許可を確認しています。

Origin: http://foo.example
Access-Control-Request-Method: POST

サーバー側でOriginの情報などからアクセスできることをレスポンスしています。

Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS

▼実際の本リクエスト(ブラウザ→サーバー)とレスポンス(サーバー→ブラウザ)の例

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

Arun


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]

ちなみにプリフライトリクエストのレスポンスにて、「Access-Control-Allow-Methods」を返していない場合などは、ブラウザ側で本リクエストは送らないようになります。

CORSによる単純リクエストと単純リクエスト以外(preflightを事前に送る)の通信について分かったかと思います。

ちなみにこのあたりは、通称「徳丸本」と呼ばれる徳丸先生が書いた本を読むとセキュリティの知識が深まります。
» 体系的に学ぶ 安全なWebアプリケーションの作り方 第2版

まとめ

ブラウザの「同一オリジンポリシー」によってクロスオリジンへのアクセス制限がありますが、CORSの機能提供によって、クロスオリジンへのリクエストが可能になりました。

主に以下のような判断をします。

  • 単純リクエスト(一定条件下のGETPOSTHEADメソッド)では、無条件にリクエスト送信が可能で、レスポンスの「Access-Control-Allow-xxxx」によってリソースの受け取り可否が判断されます。
  • 単純リクエスト以外では、preflight(プリフライトリクエスト)を送信して送信可否の許可を確認してから、本送信としてリクエストを送信するようにします。

また以下の記事で、Preflightリクエストを利用して応用したCSRF対策の解説しているので、よければ参考にしてみてください。
» カスタムヘッダーを使ったJavaScriptによるCSRF対策 (X-Form, X-Requested-With, X-Requested-By など)

ご参考になれば幸いです。

※当サイトでは一部のリンクについてアフィリエイトプログラムを利用して商品を紹介しています