🐕

5分でわかるSOPとCORS(同一オリジンポリシーとオリジン間リソース共有)

に公開

はじめに

Webアプリ開発をしていると、以下のようなエラーに誰しも遭遇したことがあると思います。

Access to XMLHttpRequest at 'http://localhost:8080/auth/login' from origin 'http://localhost:3001' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

私自身も過去に何度もCORSエラーに遭遇してきました。そこで、本記事でSOP(同一オリジンポリシー)とCORS(オリジン間リソース共有)の基礎についてまとめていきたいと思います。

オリジンとは?

SOPやCORSを理解する上で、オリジンの理解が不可欠です。

オリジンとは、WebサイトのURLに含まれる次の3つの要素で構成されます。

  • スキーム(プロトコル):http, httpsなど
  • ホスト名(ドメイン名):www.example.comなど
  • ポート番号:80, 443など

この3つの要素がすべて一致している場合、2つのリソースは同じオリジンと見なされます。どれか1つでも異なる場合、それらは異なるオリジンになります。
  
同一オリジンの例:

URL① URL② 理由
https://example.com https://example.com/home ホスト名、スキーム、ポート番号が全て同じ(パスが異なる)
https://example.com:443 https://example.com 443はHTTPSのデフォルトポートなので省略可

異なるオリジンの例:

URL① URL② 理由
http://example.com https://example.com スキームが異なる
https://example.com https://api.example.com サブドメインが異なる
https://example.com:443 https://example.com:8443 ポート番号が異なる

同一オリジン間の通信

www.bank.com というURLを入力すると、HTMLやJavaScriptがwww.bank.comのWebサーバーから読み込まれ、ブラウザにWebページが表示されます。そこで実行されたJavaScriptは、自身の提供元であるwww.bank.comにあるリソースやAPIに自由にアクセスすることができます。 
 
sop-and-cors.drawio (4).png

同一オリジン間の通信は安全であるため、制限されません。

異なるオリジン間の通信

一方で、罠リンクであるwww.attacker.comという怪しいサイトを開いてしまったとします。そこで実行されたJavaScriptが、www.bank.com にHTTPリクエストを送ったとしても、ブラウザはレスポンスの中身をJavaScriptに読ませません。
 
sop-and-cors.drawio (5).png

不正な罠リンクから勝手に銀行サービスにアクセスされたら怖いですよね。このようにセキュリティ上のリスクが大きい異なるオリジン(クロスオリジン)間の通信をSOPがブロックします。

SOP(同一オリジンポリシー)

SOPとは?

SOP(同一オリジンポリシー、Same-Origin Policy)とは、異なるオリジン間の通信の一部を制限する仕組みです。具体的には、あるオリジン上で動くJavaScriptなどのクライアントスクリプトが、別のオリジンのデータを自由に取得することを防ぐ仕組みです。
 
異なるオリジン間の通信では、以下のようにSOPによって制限されるものと、制限されないものがあります。同一オリジンポリシー (MDN)から引用。

  • 異なるオリジンへの書き込みは、概して許可されます。例えばリンクやリダイレクト、フォームの送信などがあります。まれに使用される HTTP リクエストの際はプリフライトが必要です。
  • 異なるオリジンの埋め込みは、概して許可されます。例は後述します。
  • 異なるオリジンからの読み込みは一般に許可されませんが、埋め込みによって読み取り権限がしばしば漏れてしまいます。例えば埋め込み画像の幅や高さ、埋め込みスクリプトの動作内容、あるいは埋め込みリソースでアクセス可能なものを読み取ることができます。

以下でその詳細を見ていきます。

SOPによって制限されないもの

以下のaction、src、href属性にクロスオリジンを指定することで、異なるオリジンへのアクセスが可能です。

  • <form action=...>によるフォーム送信
  • <a href=...>によるリンククリック
  • window.location.href=...によるリダイレクト
  • <img src=...>による画像の読み込み・表示
  • <scrip src=...>によるJavaScriptの読み込み・使用
  • <link href=...>によるCSSの読み込み・使用
  • <frame src=...>によるframe要素の読み込み・表示
  • <iframe src=...>によるiframe要素の読み込み・表示
  • <video src=...>による動画メディアの読み込み・表示
  • <audio src=...>による音声メディアの読み込み・表示

SOPによって制限されるもの

JavaScriptによって異なるオリジンのデータの中身を読み取ったり、操作することは禁止されています。
 
例えば:

  • JavaScriptで、別オリジンから読み込まれたiframeや画像にアクセスすること
  • JavaScript(XMLHttpRequestやFetch APIなど)を使って、別オリジンからのHTTPレスポンスの中身を読み込むこと
  • JavaScriptで、別オリジンのWeb Storage(sessionStorage や localStorage)にアクセスすること

など。

CORS(オリジン間リソース共有)

しかしWebサービスが大規模化してくると、APIのサブドメイン化や外部APIとの連携などの需要により、SOPの制限を超えて異なるオリジン間でも安全にデータをやり取りできる仕組みが必要になりました。そこで登場したのが、CORS(オリジン間リソース共有)です。

CORSとは?

CORS(オリジン間リソース共有、Cross-Origin Resource Sharing)は、サーバー側でアクセスを許可するオリジンを指定することで、異なるオリジン間での安全なデータ共有を可能にする仕組みです。
 
CORSでは、リクエストの種類(使用しているメソッド、ヘッダなど)に応じて異なる処理が行われます。主に単純リクエストプリフライトリクエストの2種類に分類され、それぞれサーバーとの通信の流れやヘッダのやり取りに違いがあります。

以下で、これらの違いについて説明します。

単純リクエスト(Simple Request)

以下の条件を満たす単純リクエストは、後述のプリフライトリクエストなしで直接送信することができます。

  • メソッドが次のいずれかであること:GET, POST, HEAD
  • 以下のヘッダのみを使用していること
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
  • Cotent-Typeヘッダは以下のいずれがであること
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

プリフライトリクエスト(Preflight request)

クロスオリジン間での通信において、リクエストが単純リクエストの条件を満たさない場合、実際のリクエストを送る前にプリフライトリクエストを送信する必要があります。

プリフライトリクエストとは、OPTIONS メソッドを使用して、これから送信するリクエストがサーバーによって許可されているかを事前に確認するリクエストのことです。サーバーが適切な許可ヘッダーを返せば、続けて本リクエストが送信されます。

プリフライトリクエストの例

以下のような条件でのプリフライトリクエストの送信例を考えます。

  • フロントエンド:https://www.example.com
  • バックエンド:https://api.example.com
  • フロントエンドが POST /loginContent-Type: application/jsonで送信する

以下がシーケンス図になります。
 
preflight-request.drawio (3).png

1. プリフライトリクエストの送信

まず本リクエストは、Content-Type: application/jsonであり単純リクエストの条件を満たさないため、以下のようなプリフライトリクエストが送信されます。

OPTIONS /login HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

プリフライトリクエストが以下のヘッダを送信することで、サーバー側で本リクエストが許可されたオリジン・メソッド・ヘッダを使用しているかを事前に確認することができます。

リクエストヘッダ 役割
Origin リクエスト送信元のオリジン
Access-Control-Request-Method 本リクエストで使用するHTTPメソッド
Access-Control-Request-Headers 本リクエストで使用するリクエストヘッダ

2. プリフライトリクエストへのレスポンス

サーバー側では「許可するオリジン、HTTPメソッド、HTTPヘッダ」など、CORSに関する設定がされています。
 
(↓Spring SecurityのCORS設定の例)

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("https://www.example.com"));
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    configuration.setAllowCredentials(true);

    //その他の設定
}

その内容をもとに、許可するオリジン・メソッド・ヘッダをレスポンスヘッダで返します。

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Credentials: true
レスポンスヘッダ 役割
Access-Control-Allow-Origin レスポンスの取得を許可するオリジンを指定
Access-Control-Allow-Methods 許可されるHTTPメソッドを指定
Access-Control-Allow-Headers 許可されるHTTPヘッダを指定
Access-Control-Allow-Credentials 認証情報付きのリクエストを受け付けることを通知

3. 実際のリクエストの送信

そして、レスポンスヘッダを見ると、https://www.example.comからのContent-Typeヘッダ付きのPOSTリクエストに対して、レスポンスの中身の取得が許可されていることが確認できます。

確認できたので、実際のリクエストを送信します。

POST /login HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "password"
}

認証情報を含むリクエスト

デフォルトでは、ブラウザはクロスオリジンに対するリクエストでは、Cookieヘッダなどの認証情報を送信しません。
 
認証情報を送信するためには、JavaScriptでfetch()XMLHttpRequest (AxiosやAjax)を使用する際に、以下のような設定を追加する必要があります。

  • fetch()credentials: "include"
  • XMLHttpRequestwithCredentials:true

そして、この時ブラウザはAccess-Control-Allow-Credentials: trueヘッダを持たないレスポンスの受け取りを拒否するので、サーバー側でAccess-Control-Allow-Credentials: trueヘッダを追加する必要があります。

注意点として、認証情報を含むリクエストに対して、サーバー側でAccess-Control-Allow-Origin: * のようにワイルドカードを指定すると、ブラウザはレスポンスの受け取りを拒否し、CORSエラーを発生させます。(私は何度もこのエラーに遭遇しました、、)サーバー側でAccess-Control-Allow-Origin: https://www.example.comのように、明示的にオリジンを指定しなければいけません。

参考

同一オリジンポリシー (MDN)
オリジン間リソース共有 (CORS) (MDN)
安全なWebアプリケーションの作り方 (徳丸浩)
これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎 (Qiita)
Same-Origin Policy And Cross-Origin Resource Sharing (CORS) (Security Journey)

Discussion