フロントエンドとバックエンドが分離した現代の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.comhttp://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設定が必要"]

具体的なケースとしては以下が挙げられます。

  1. フロントエンドとAPIの分離: SPAがAPIサーバーにリクエストを送信する場合
  2. マイクロサービス間通信: ブラウザから複数のマイクロサービスにアクセスする場合
  3. CDNからのリソース取得: 別ドメインのCDNからフォントやデータを取得する場合
  4. サードパーティ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つの異なるフローが存在します。「単純リクエスト」と「プリフライトリクエスト」です。

単純リクエストの条件

以下のすべての条件を満たすリクエストは「単純リクエスト」として扱われ、プリフライトなしで直接送信されます。

  1. HTTPメソッド: GETHEADPOST のいずれか
  2. リクエストヘッダー: 以下のヘッダーのみ使用
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(下記の値に限る)
    • Range(単純範囲ヘッダー値のみ)
  3. Content-Type: 以下の値のいずれか
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  4. その他: XMLHttpRequest.upload にイベントリスナーが登録されていない、ReadableStream が使用されていない

これらの条件は、HTMLの <form> 要素で送信可能なリクエストと同等の制約を設けています。つまり、従来からブラウザが許可していた範囲内のリクエストです。

単純リクエストの例

以下のコードは単純リクエストの例です。

1
2
3
4
5
6
7
// 単純リクエストの例
fetch('https://api.example.com/users', {
  method: 'GET',
  headers: {
    'Accept': 'application/json'
  }
});

このリクエストに対するHTTP通信は以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
GET /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Accept: application/json

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"users": [...]}

プリフライトリクエストとは

単純リクエストの条件を満たさないリクエストは、本番のリクエストを送信する前に「プリフライトリクエスト」と呼ばれる事前確認が行われます。これは OPTIONS メソッドを使用した確認リクエストです。

プリフライトリクエストが必要になる代表的なケースは以下のとおりです。

  • PUTDELETEPATCH などのメソッドを使用する場合
  • Content-Type: application/json を使用する場合
  • カスタムヘッダー(AuthorizationX-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

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

以下のコードはプリフライトリクエストが発生するケースです。

1
2
3
4
5
6
7
8
9
// プリフライトリクエストが発生する例
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  },
  body: JSON.stringify({ name: 'John Doe', email: 'john@example.com' })
});

このリクエストに対する実際のHTTP通信は以下のようになります。

まず、プリフライトリクエスト(OPTIONSリクエスト)が送信されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

プリフライトが成功すると、実際のリクエストが送信されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

{"name": "John Doe", "email": "john@example.com"}

HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json

{"id": 123, "name": "John Doe", "email": "john@example.com"}

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 ヘッダーには、主に以下の設定パターンがあります。

1
2
3
4
5
# すべてのオリジンを許可(公開APIなど)
Access-Control-Allow-Origin: *

# 特定のオリジンのみ許可
Access-Control-Allow-Origin: https://app.example.com

重要な点として、Access-Control-Allow-Origin: * と資格情報(Cookie、Authorizationヘッダーなど)を含むリクエストは併用できません。資格情報を含むリクエストを許可する場合は、必ず特定のオリジンを指定する必要があります。

1
2
3
# 資格情報を含むリクエストを許可する場合
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

資格情報を含むリクエスト

Cookie、HTTPベーシック認証、クライアント証明書などの資格情報を含むクロスオリジンリクエストには、追加の設定が必要です。

フロントエンド側の設定

fetch() APIを使用する場合は、credentials オプションを include に設定します。

1
2
3
4
5
// 資格情報を含むリクエスト
fetch('https://api.example.com/users/me', {
  method: 'GET',
  credentials: 'include'  // Cookieを送信
});

XMLHttpRequest を使用する場合は、withCredentials プロパティを true に設定します。

1
2
3
4
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users/me');
xhr.withCredentials = true;
xhr.send();

サーバー側の設定

資格情報を含むリクエストを許可するには、サーバーで以下のヘッダーを返す必要があります。

1
2
Access-Control-Allow-Origin: https://app.example.com  # ワイルドカード不可
Access-Control-Allow-Credentials: true

資格情報を含むリクエストでは、以下の制約があります。

  1. Access-Control-Allow-Origin にワイルドカード(*)は使用できない
  2. Access-Control-Allow-Headers にワイルドカード(*)は使用できない
  3. Access-Control-Allow-Methods にワイルドカード(*)は使用できない
  4. Access-Control-Expose-Headers にワイルドカード(*)は使用できない

CORSエラーのデバッグ方法

CORSエラーが発生した場合のデバッグ方法を解説します。

ブラウザのコンソールを確認する

CORSエラーの詳細は、ブラウザの開発者ツールのコンソールに表示されます。

1
2
3
Access to fetch at 'https://api.example.com/users' from origin 
'https://app.example.com' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

このエラーメッセージから、以下の情報を読み取れます。

  • リクエスト先URL: https://api.example.com/users
  • リクエスト元オリジン: https://app.example.com
  • 原因: Access-Control-Allow-Origin ヘッダーが存在しない

Network タブでリクエストを確認する

開発者ツールのNetworkタブで、実際のリクエストとレスポンスを確認できます。プリフライトリクエストが発生している場合は、OPTIONSリクエストも表示されます。

確認すべきポイントは以下のとおりです。

  1. OPTIONSリクエストのステータスコード(200や204が正常)
  2. レスポンスヘッダーに Access-Control-Allow-Origin が含まれているか
  3. Access-Control-Allow-Methods に必要なメソッドが含まれているか
  4. 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設定方法と、開発環境でのプロキシ設定について解説します。

参考リンク