フロントエンドとバックエンドが分離した現代のWeb開発では、APIサーバーとフロントエンドのドメインが異なることが一般的です。そのとき必ず遭遇するのが「CORSエラー」です。
「Access to fetch at ‘https://api.example.com’ from origin ‘https://app.example.com’ has been blocked by CORS policy」というエラーメッセージを見たことがある方も多いのではないでしょうか。
この記事では、CORSエラーの根本原因である「同一オリジンポリシー(Same-Origin Policy)」から始めて、オリジンの定義、CORSが必要になる場面、そしてプリフライトリクエストの仕組みまでを体系的に解説します。MDN Web DocsやFetch Standardなどの一次情報に基づいた技術的な裏付けとともにお伝えしますので、この記事を読み終えるころにはCORSエラーの原因を正確に理解し、適切なCORS設定ができるようになっているでしょう。
同一オリジンポリシーとは
同一オリジンポリシー(Same-Origin Policy、SOP)は、Webブラウザに実装されている重要なセキュリティ機構です。あるオリジンから読み込まれたドキュメントやスクリプトが、別のオリジンにあるリソースにアクセスする方法を制限します。
なぜ同一オリジンポリシーが必要なのか
もし同一オリジンポリシーが存在しなかった場合、悪意のあるWebサイトが以下のような攻撃を簡単に実行できてしまいます。
| ステップ | 説明 |
|---|---|
| 1 | ユーザーがオンラインバンキングにログイン(セッション確立) |
| 2 | ユーザーが悪意のあるサイト evil.com を訪問 |
| 3 | evil.com のJavaScriptが bank.com のAPIを呼び出し |
| 4 | ブラウザはCookieを自動送信(ユーザーのセッション情報付き) |
| 5 | evil.com がユーザーの口座情報を取得・送金を実行 |
同一オリジンポリシーはこのような攻撃を防ぐため、異なるオリジン間でのリソースアクセスを制限します。これにより、悪意のあるWebサイトがユーザーの認証済みセッションを利用して他のサイトのデータを読み取ることを防止できます。
同一オリジンポリシーの制限対象
同一オリジンポリシーが制限するのは、主にJavaScriptによる以下の操作です。
fetch()やXMLHttpRequestによるHTTPリクエストのレスポンス読み取り<iframe>内の別オリジンドキュメントへのDOM操作Canvasに描画された別オリジンの画像データの読み取り- Web Storageや IndexedDB への別オリジンからのアクセス
一方で、以下の操作は同一オリジンポリシーによって制限されません。
<script src="...">による別オリジンのJavaScriptファイルの実行<link rel="stylesheet" href="...">による別オリジンのCSSファイルの適用<img src="...">による別オリジンの画像の表示<form action="...">による別オリジンへのフォーム送信
オリジンの定義を正確に理解する
同一オリジンポリシーを正しく理解するためには、「オリジン」の定義を正確に把握することが重要です。
オリジンを構成する3つの要素
オリジン(Origin)は、以下の3つの要素の組み合わせで定義されます。
flowchart LR
subgraph Origin["オリジンの構成要素"]
URL["https://example.com:443/path/to/page.html"]
A["スキーム<br>https"]
B["ホスト<br>example.com"]
C["ポート<br>443"]
D["パス<br>/path/to/page.html<br>(オリジンには含まれない)"]
end
Note["オリジン = スキーム + ホスト + ポート"]これら3つの要素(スキーム、ホスト、ポート番号)がすべて一致する場合のみ「同一オリジン」とみなされます。パス部分はオリジンの判定に含まれません。
同一オリジンの判定例
以下の表は、http://store.company.com/dir/page.html を基準として、各URLが同一オリジンかどうかを判定した例です。
| URL | 判定結果 | 理由 |
|---|---|---|
http://store.company.com/dir2/other.html |
同一オリジン | パスのみ異なる |
http://store.company.com/dir/inner/another.html |
同一オリジン | パスのみ異なる |
https://store.company.com/page.html |
異なるオリジン | スキームが異なる(http vs https) |
http://store.company.com:81/dir/page.html |
異なるオリジン | ポートが異なる(80 vs 81) |
http://news.company.com/dir/page.html |
異なるオリジン | ホストが異なる |
ポート番号の省略ルール
HTTPとHTTPSにはデフォルトポートが定められています。
- HTTP: 80番ポート
- HTTPS: 443番ポート
URLにポート番号が明示されていない場合、これらのデフォルトポートが使用されます。そのため、http://example.com と http://example.com:80 は同一オリジンとして扱われます。
CORSとは何か
CORS(Cross-Origin Resource Sharing、オリジン間リソース共有)は、同一オリジンポリシーを安全に緩和するための仕組みです。サーバーが特定のオリジンからのクロスオリジンリクエストを許可することで、異なるオリジン間でのリソース共有を可能にします。
CORSが必要になる典型的なシーン
現代のWeb開発では、以下のようなケースでCORSが必要になります。
flowchart LR
subgraph CORS["典型的なアーキテクチャ"]
A["フロントエンド<br>https://app.example.com<br>React, Vue, Angular など"] -->|APIリクエスト| B["バックエンド<br>https://api.example.com<br>REST API, GraphQL"]
end
Note["オリジンが異なる → CORS設定が必要"]具体的なケースとしては以下が挙げられます。
- フロントエンドとAPIの分離: SPAがAPIサーバーにリクエストを送信する場合
- マイクロサービス間通信: ブラウザから複数のマイクロサービスにアクセスする場合
- CDNからのリソース取得: 別ドメインのCDNからフォントやデータを取得する場合
- サードパーティAPI連携: 外部サービスのAPIをフロントエンドから直接呼び出す場合
CORSの基本的な仕組み
CORSは、HTTPヘッダーを使用してクロスオリジンアクセスを制御します。基本的な流れは以下のとおりです。
sequenceDiagram
participant Client as クライアント(ブラウザ)
participant Server as サーバー
Note over Client,Server: CORSの基本フロー(単純リクエストの場合)
Client->>Server: GET /api/users<br>Origin: https://app.example.com
Note over Client: オリジンを自動付与
Server-->>Client: 200 OK<br>Access-Control-Allow-Origin: https://app.example.com
Note over Server: 許可するオリジンを返答
Note over Client: ブラウザがレスポンスを検証<br>OriginとAccess-Control-Allow-Originが一致<br>→ JavaScriptからレスポンスにアクセス可能ブラウザは自動的に Origin ヘッダーをリクエストに付与し、サーバーからのレスポンスに含まれる Access-Control-Allow-Origin ヘッダーと照合します。一致すればJavaScriptからレスポンスにアクセスでき、一致しなければCORSエラーとなります。
単純リクエストとプリフライトリクエスト
CORSでは、リクエストの内容によって2つの異なるフローが存在します。「単純リクエスト」と「プリフライトリクエスト」です。
単純リクエストの条件
以下のすべての条件を満たすリクエストは「単純リクエスト」として扱われ、プリフライトなしで直接送信されます。
- HTTPメソッド:
GET、HEAD、POSTのいずれか - リクエストヘッダー: 以下のヘッダーのみ使用
AcceptAccept-LanguageContent-LanguageContent-Type(下記の値に限る)Range(単純範囲ヘッダー値のみ)
- Content-Type: 以下の値のいずれか
application/x-www-form-urlencodedmultipart/form-datatext/plain
- その他:
XMLHttpRequest.uploadにイベントリスナーが登録されていない、ReadableStreamが使用されていない
これらの条件は、HTMLの <form> 要素で送信可能なリクエストと同等の制約を設けています。つまり、従来からブラウザが許可していた範囲内のリクエストです。
単純リクエストの例
以下のコードは単純リクエストの例です。
|
|
このリクエストに対するHTTP通信は以下のようになります。
|
|
プリフライトリクエストとは
単純リクエストの条件を満たさないリクエストは、本番のリクエストを送信する前に「プリフライトリクエスト」と呼ばれる事前確認が行われます。これは OPTIONS メソッドを使用した確認リクエストです。
プリフライトリクエストが必要になる代表的なケースは以下のとおりです。
PUT、DELETE、PATCHなどのメソッドを使用する場合Content-Type: application/jsonを使用する場合- カスタムヘッダー(
Authorization、X-Custom-Headerなど)を使用する場合
プリフライトリクエストのフロー
プリフライトリクエストを含む通信フローを図解すると以下のようになります。
sequenceDiagram
participant Browser as ブラウザ
participant Server as サーバー
Note over Browser,Server: 1. プリフライトリクエスト(事前確認)
Browser->>Server: OPTIONS /api/users<br>Origin: https://app.example.com<br>Access-Control-Request-Method: POST<br>Access-Control-Request-Headers: Content-Type, Authorization
Note over Browser,Server: 2. プリフライトレスポンス
Server-->>Browser: 204 No Content<br>Access-Control-Allow-Origin: https://app.example.com<br>Access-Control-Allow-Methods: POST, GET, OPTIONS<br>Access-Control-Allow-Headers: Content-Type, Authorization<br>Access-Control-Max-Age: 86400
Note over Browser,Server: 3. 実際のリクエスト
Browser->>Server: POST /api/users<br>Origin: https://app.example.com<br>Content-Type: application/json<br>Authorization: Bearer xxx
Note over Browser,Server: 4. 実際のレスポンス
Server-->>Browser: 200 OK<br>Access-Control-Allow-Origin: https://app.example.comプリフライトリクエストの実例
以下のコードはプリフライトリクエストが発生するケースです。
|
|
このリクエストに対する実際のHTTP通信は以下のようになります。
まず、プリフライトリクエスト(OPTIONSリクエスト)が送信されます。
|
|
プリフライトが成功すると、実際のリクエストが送信されます。
|
|
CORSに関連するHTTPヘッダー
CORSでは、リクエストヘッダーとレスポンスヘッダーの両方が使用されます。
リクエストヘッダー
ブラウザが自動的に付与するヘッダーです。
| ヘッダー名 | 説明 |
|---|---|
Origin |
リクエスト元のオリジン |
Access-Control-Request-Method |
プリフライトで使用。実際のリクエストで使用するHTTPメソッド |
Access-Control-Request-Headers |
プリフライトで使用。実際のリクエストで使用するヘッダー |
レスポンスヘッダー
サーバーが設定するヘッダーです。
| ヘッダー名 | 説明 |
|---|---|
Access-Control-Allow-Origin |
アクセスを許可するオリジン(* または特定のオリジン) |
Access-Control-Allow-Methods |
許可するHTTPメソッド |
Access-Control-Allow-Headers |
許可するリクエストヘッダー |
Access-Control-Allow-Credentials |
資格情報(Cookie等)を含むリクエストを許可するか |
Access-Control-Expose-Headers |
JavaScriptからアクセス可能にするレスポンスヘッダー |
Access-Control-Max-Age |
プリフライトリクエストの結果をキャッシュする秒数 |
Access-Control-Allow-Originの設定パターン
Access-Control-Allow-Origin ヘッダーには、主に以下の設定パターンがあります。
|
|
重要な点として、Access-Control-Allow-Origin: * と資格情報(Cookie、Authorizationヘッダーなど)を含むリクエストは併用できません。資格情報を含むリクエストを許可する場合は、必ず特定のオリジンを指定する必要があります。
|
|
資格情報を含むリクエスト
Cookie、HTTPベーシック認証、クライアント証明書などの資格情報を含むクロスオリジンリクエストには、追加の設定が必要です。
フロントエンド側の設定
fetch() APIを使用する場合は、credentials オプションを include に設定します。
|
|
XMLHttpRequest を使用する場合は、withCredentials プロパティを true に設定します。
|
|
サーバー側の設定
資格情報を含むリクエストを許可するには、サーバーで以下のヘッダーを返す必要があります。
|
|
資格情報を含むリクエストでは、以下の制約があります。
Access-Control-Allow-Originにワイルドカード(*)は使用できないAccess-Control-Allow-Headersにワイルドカード(*)は使用できないAccess-Control-Allow-Methodsにワイルドカード(*)は使用できないAccess-Control-Expose-Headersにワイルドカード(*)は使用できない
CORSエラーのデバッグ方法
CORSエラーが発生した場合のデバッグ方法を解説します。
ブラウザのコンソールを確認する
CORSエラーの詳細は、ブラウザの開発者ツールのコンソールに表示されます。
|
|
このエラーメッセージから、以下の情報を読み取れます。
- リクエスト先URL:
https://api.example.com/users - リクエスト元オリジン:
https://app.example.com - 原因:
Access-Control-Allow-Originヘッダーが存在しない
Network タブでリクエストを確認する
開発者ツールのNetworkタブで、実際のリクエストとレスポンスを確認できます。プリフライトリクエストが発生している場合は、OPTIONSリクエストも表示されます。
確認すべきポイントは以下のとおりです。
- OPTIONSリクエストのステータスコード(200や204が正常)
- レスポンスヘッダーに
Access-Control-Allow-Originが含まれているか Access-Control-Allow-Methodsに必要なメソッドが含まれているかAccess-Control-Allow-Headersに必要なヘッダーが含まれているか
よくあるCORSエラーと解決策
| エラー内容 | 原因 | 解決策 |
|---|---|---|
| No ‘Access-Control-Allow-Origin’ header | サーバーがCORSヘッダーを返していない | サーバー側でCORSヘッダーを設定する |
| Origin is not allowed | 許可されていないオリジンからのリクエスト | サーバー側で該当オリジンを許可する |
| Method is not allowed | 許可されていないHTTPメソッド | Access-Control-Allow-Methods に追加する |
| Header is not allowed | 許可されていないヘッダー | Access-Control-Allow-Headers に追加する |
| Credentials flag is true, but Allow-Origin is wildcard | 資格情報付きリクエストでワイルドカード使用 | 特定のオリジンを指定する |
まとめ
この記事では、同一オリジンポリシーとCORSについて以下の内容を解説しました。
同一オリジンポリシー(SOP)について
- ブラウザに実装されたセキュリティ機構
- オリジンは「スキーム + ホスト + ポート」の組み合わせで定義される
- 悪意のあるサイトからのデータ窃取を防止する
CORSについて
- 同一オリジンポリシーを安全に緩和する仕組み
- HTTPヘッダーを使用してクロスオリジンアクセスを制御
- フロントエンドとAPIの分離などで必要になる
プリフライトリクエストについて
- 単純リクエストの条件を満たさない場合に発生
- OPTIONSメソッドで事前確認を行う
Access-Control-Max-Ageでキャッシュ可能
CORSエラーに遭遇した際は、まずブラウザのコンソールで詳細なエラーメッセージを確認し、リクエストとレスポンスのヘッダーを照合することで原因を特定できます。次回の記事では、実際のサーバーサイドフレームワーク(Express、Django、Spring Boot)でのCORS設定方法と、開発環境でのプロキシ設定について解説します。