はじめに
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に自由にアクセスすることができます。
同一オリジン間の通信は安全であるため、制限されません。
異なるオリジン間の通信
一方で、罠リンクであるwww.attacker.com
という怪しいサイトを開いてしまったとします。そこで実行されたJavaScriptが、www.bank.com
にHTTPリクエストを送ったとしても、ブラウザはレスポンスの中身をJavaScriptに読ませません。
不正な罠リンクから勝手に銀行サービスにアクセスされたら怖いですよね。このようにセキュリティ上のリスクが大きい異なるオリジン(クロスオリジン)間の通信を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=...>
による音声メディアの読み込み・表示
<img>
や <iframe>
タグなどを使用して別のオリジンのリソースを自分のページ内に表示する(埋め込み)ことは可能ですが、JavaScriptなどでその中身を取得・操作(読み込み)することはSOPによって禁止されています。
SOPによって制限されるもの
JavaScriptによって異なるオリジンのデータの中身を読み取ったり、操作することは禁止されています。
例えば:
- JavaScriptで、別オリジンから読み込まれたiframeや画像にアクセスすること
- JavaScript(XMLHttpRequestやFetch APIなど)を使って、別オリジンからのHTTPレスポンスの中身を読み込むこと
- JavaScriptで、別オリジンのWeb Storage(sessionStorage や localStorage)にアクセスすること
など。
XMLHttpRequest(AxiosやAjax)などを使用して、異なるオリジンにHTTPリクエストを送信すること自体は可能ですが、ブラウザはSOPのルールに従って、JavaScriptによるレスポンスの中身の取得をブロックします。
ただし、CORS仕様により、サーバー側が明示的に許可を出している場合に限り、JavaScriptはレスポンスの中身を取得できます。
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 /login
をContent-Type: application/json
で送信する
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ヘッダなどの認証情報を送信しません。
認証情報を送信するためには、fetch()
やXMLHttpRequest (AxiosやAjax)
を使用したリクエストの送信時には、以下のような設定を追加する必要があります。
-
fetch()
:credentials: "include"
-
XMLHttpRequest
:withCredentials: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)