😐

CORSでCookieの送受信をする

2024/09/22に公開

CORS(Cross-Origin Resource Sharing)とは

ブラウザーに実装されているJavaScriptは、同じオリジンにしかアクセスできない。これを同一(Same)オリジン(Origin)ポリシー(Policy)という。この同一オリジンポリシーの制限(セキュリティ)を緩和することをCORSという。

クロス(Cross)オリジン(Origin)とは、異なるオリジン間ということ。オリジンの定義は後述。
リソース(Resource)をシェアする(Sharing)とは、DOMやJSONなどのリソースを使うことを許可すること。

普通にWebアプリを開発していると、同一オリジンポリシー(JavaScriptは同じオリジン間のリソースにしかアクセスできない)は非常に煩わしい。

CORSが必要な理由

JavaScriptから自分のサイトのリソースを自由に使わせると、コピーサイトを簡単に作成される。

また、訪問者のCPUリソースを使って、DOMを解析させるなどの行為も可能で、認証の必要なサイト内のリソースを取得することも可能にしてしまう。

おそらく一番問題とされているのは、Cookieの取得なのではなかろうか?ログイン情報などはCookieに保存されていることが多い。

認証が必要なサイトを作成している場合は、CORSの扱いを慎重にした方が良い。

Same-Origin Policy:同一オリジンポリシーとは

同一オリジンポリシー(Same-Origin Policy, SOP)とは、異なるオリジン(異なるドメインやポート、プロトコル)間でのデータのやり取りを制限するポリシー。サブドメインも異なるドメインとなる。
サブドメインなら良いように思えるが、ブラウザーがサブドメインか判断できないからだろう。

このポリシーにより、あるオリジンからのスクリプトが他のオリジンのリソースやデータにアクセスすることを防ぎ、クロスサイトスクリプティング(XSS)やクロスサイトリクエストフォージェリ(CSRF)といった攻撃を防ぐ。

オリジンの定義

オリジンは、次の3つの要素の組み合わせによって定義される。

  1. プロトコル(例: http, https, ftp, ssh)
  2. ドメイン名(例: example.com, api.example.com)
  3. ポート番号(例: 80, 443, 8080)

同じドメインなら、プロトコルやポート番号が変わっても問題ないと思えるが、ダメである。なぜかは不明。どんな攻撃が成立するのか不明。

同一オリジンポリシーの具体的な制限

同一オリジンポリシーによって、あるオリジン上のWebページが他のオリジンに対して以下のような操作を行うことが制限される。

  1. DOM操作: 他のオリジンのWebページのHTMLやDOM要素にアクセスすることができない。
    例: example.comのJavaScriptが、ex.example.comのDOMを変更したり、内容を読み取ったりすること。(JavaScriptだけでコピーサイトを作れる。しかしサーバーサイドクロールすればコピーサイトはいくらでも作れる)
  2. Cookieの共有: 他のオリジンのCookieにはアクセスできない。
    例: example.comで発行されたCookieは、ex.example.comのJavaScriptからは読み取れない。(読み取れたら情報漏洩)
  3. AJAXリクエスト: クロスオリジンのXMLHttpRequest(fetch()を含む)には制限がかかり、他のオリジンに対してのリクエストやレスポンスデータの処理は同一オリジンでなければ許可されない。
    例: example.comからapi.example.comに直接リクエストを送っても、ポリシーによってアクセスがブロックされる。(これがOKなら、1.のDOM操作ブロックを回避できる)

同一オリジンポリシーを回避する方法

  1. CORS(Cross-Origin Resource Sharing)
  2. JSONP(JSON with Padding)
  3. PostMessage API

CORS(Cross-Origin Resource Sharing)

CORSは、サーバー側でアクセスを許可するオリジンを明示する。ブラウザーは、オリジンが許可されているかチェックする。許可されていなければ、ブラウザーがアクセスをブロックする。

例:サーバー側で、https://example.comからのアクセスを許可する

Access-Control-Allow-Origin: https://example.com

JSONP(JSON with Padding)

JSONPは、JavaScriptを使った古いクロスドメインリクエストの方法。サーバーからスクリプトとしてデータを返し、クライアント側でそれを<script>タグを使って読み込むことで実現する。セキュリティ上の理由から現代ではあまり使用されない。

PostMessage API

異なるオリジン間で安全にメッセージを送受信する方法。iframeを使ったクロスドメイン通信でよく使われる。この記事では扱わない。

// iframe内でメッセージを送信
window.parent.postMessage('Hello from iframe', 'https://parent.com');

// 受信側でメッセージを受け取る
window.addEventListener('message', (event) => {
  if( event.origin === 'https://iframe.com' ){
    console.log(event.data); // 'Hello from iframe'
  }
});

前提条件

  • JavaScriptのfetch関数を使ってJSONを取得する
  • PHPでJSONを返す

一般的なCORSリクエストの手順

  1. JavaScriptでfetch()を使ってJSONを要求する
  2. サーバーがCORSヘッダーを含めたJSONを返す
  3. ブラウザーは、サーバーが送信したCORSヘッダーを検証する
    • 許可された場合は、JSONが読み込める
    • 許可されない場合は、リクエストが失敗する

ポイントは、サーバーがJSONを返しても、ブラウザーがCORSヘッダーをチェックし不正と判断したら、fetch()はブラウザーの判断で失敗にされる。

シンプル・リクエスト

CORSの仕組みでは、いくつかの条件に合致する場合、リクエストは「シンプル・リクエスト」として扱われる。シンプル・リクエストでは、特別な処理なしにすぐにリクエストが送信され、CORSの許可が行われる。

シンプル・リクエストの条件

  • HTTPメソッドは、GETPOSTHEAD のいずれかであること
  • Content-Typeは、text/plainmultipart/form-dataapplication/x-www-form-urlencoded のいずれかであること

プリフライト・リクエスト

シンプル・リクエスト以外のリクエスト(特にカスタムヘッダーやPUTDELETEなどのメソッドを使う場合)は、「プリフライト・リクエスト」が必要。プリフライト・リクエストでは、ブラウザーが最初にサーバーに対してOPTIONSメソッドを使い、リクエストを許可するかどうかを確認する。

  1. JavaScriptでfetch()を使いOPTIONSリクエストをサーバーに送信する
  2. サーバーが許可されたリクエストかどうかを確認する
  3. サーバーがAccess-Control-Allow-OriginAccess-Control-Allow-MethodsなどのCORSヘッダーを含めて応答する
  4. クライアントはその応答に基づいて本来のリクエストを送信するかどうか決定する
fetch('https://example.com/api/data', {
  method: 'OPTIONS'
});

クレデンシャル付きリクエスト

認証情報(クッキーやHTTP認証)を含むリクエスト。この場合、Access-Control-Allow-Credentials: trueをサーバー側が指定し、クライアントもcredentialsオプションを使って認証情報を送信する。

fetch('https://example.com/api/data', {
  method: 'GET',
  credentials: 'include' // クッキーなどの認証情報を含める
});

主なCORSヘッダー

Access-Control-Allow-Origin

許可(Allow)するオリジン(Origin)を指定する。

Access-Control-Allow-Origin: https://example.com

全てのオリジン(ドメイン)を許可する場合は、ワイルドカード(*)を指定する。

Access-Control-Allow-Origin: *

Access-Control-Allow-Methods

許可(Allow)するHTTPメソッド(GET, POST, PUT, DELETE, OPTION)を指定する。

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTION

Access-Control-Allow-Headers

JavaScriptが送信できるカスタムヘッダーを指定する。
Content-TypeAuthorization などは、デフォルトで許可されていないヘッダーなので、許可する場合に指定する。例えばJSONなどは、いきなりサーバーに送信することは許可されていない。

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Allow-Credentials

認証情報(クッキーやHTTP認証)をクロスオリジンリクエストで許可するかどうかを指定する。
trueに設定することで、クロスオリジンでもクッキーやセッション情報を送信できるようになる。

Access-Control-Allow-Credentials: true

Access-Control-Expose-Headers

ブラウザーに公開するサーバーのレスポンスヘッダーを指定する。デフォルトでは、サーバーのレスポンスヘッダーの一部しかクライアントに公開されないが、このヘッダーで公開するものを増やすことができる。どんな利点があるのかよく分からない。

Access-Control-Expose-Headers: X-Custom-Header

JavaScript

  • credentials: includeでCookieを送信する
  • OPTION: CORSに対応しているか調べる
( async function(){
  let uri  = "https://api.example.com/index.php";
  let temp = await fetch(uri, {credentials:'include', method: 'OPTIONS'}); 
  let resp = await fetch(uri, {credentials:'include', method: 'GET'    }); 
  let json = await resp.json();
  console.log(json);
})();

PHP

<?php
//  全てのサイトを許可する
header("Access-Control-Allow-Origin: *");
//  GET, POST, OPTIONSだけ許可する
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
//  Content-Typeを許可しておくと、クライアントがJSONを送信することができる
header("Access-Control-Allow-Headers: Content-Type, Authorization");
//  データ部(ボディ部)のMIMEの設定
header('Content-type: text/json');

//	Preflight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
	exit;
}

//	...
$json = [];
$json['status'] = true;
$json['errros'] = [];
$json['result'] = null;

//	...
echo json_encode($json);

CORSでCookieを送信する設定について

  • samesite=None: Noneでなければならない
  • secure=true: HTTPSでなければならない
  • httponly=false: trueだとJavaScriptでは送信されない

samesite

  • None: クロスサイトリクエストでもCookieが送信される(必ずSecureが必要)
  • Lax: クロスサイトリクエストでは制限がある。リンククリックによるナビゲーションでだけCookieが送信される
  • Strict: 同一のオリジンによるリクエストしかCookieの送信はしない

Discussion