誰もが経験したことでしょう:APIリクエストを送信し、応答を待っていると、突然ブラウザのコンソールに「 CORSエラー」が表示されるのです。
多くの開発者は、まず手っ取り早い解決策を探す傾向があります:`Access-Control-Allow-Origin: *` を追加して済ませようとするのです。しかし、このアプローチは本質を完全に捉えていません。CORSは単なる設定上の障害ではなく、これまで構築された中で最も重要なブラウザセキュリティ機構の一つなのです。
CORS(クロスオリジンリソース共有)は、ユーザーを保護しつつ、ウェブアプリケーション間の正当なクロスドメイン通信を可能にするために存在します。しかし、それはしばしば誤解されたり、誤って設定されたり、あるいは「回避」すべきバグのように扱われたりしています。
でも、もうそうじゃない。
このガイドでは、基本を超えた内容を解説します。以下のことを学べます:
- CORSが存在する理由と、同一生成元ポリシー(SOP)からの進化の経緯
- ブラウザとサーバーが実際にクロスオリジンアクセスをどのように交渉するか
- CORSの設定が「見た目は正しい」にもかかわらず失敗する原因
- 事前フライトリクエスト、認証情報、およびブラウザの挙動を安全に処理する方法
最終的には、CORSの設定方法だけでなく、その動作原理や、それを踏まえた安全なAPI設計の方法も理解できるようになります。
CORSとは何か(そしてなぜ存在するのか)
CORS(Cross-Origin Resource Sharing)は、あるオリジンからのWebアプリケーションが別のオリジンからのリソースに安全にアクセスする方法を定義するブラウザセキュリティ標準です。
CORSセキュリティを理解するには、まずそれがなぜ作られたのかを知る必要があります。
APIやマイクロサービスがウェブを席巻するずっと前から、ブラウザは「同一生成元ポリシー(SOP)」と呼ばれるシンプルなルールに従っていた。
このポリシーは、ウェブページが同じオリジン(同じプロトコル、ドメイン、ポート)からのみデータを送受信できると定めていた。
例えば:
この制限は、初期のウェブにおいて、ほとんどのウェブサイトが単一構造だった時代に完全に理にかなっていた。単一のサイトが、フロントエンド、バックエンド、アセットのすべてを1つのドメイン下にホストしていたのである。
しかし、APIやマイクロサービス、サードパーティ統合によってウェブが進化するにつれ、この同じルールが障壁となった。開発者はフロントエンドアプリケーションが他のドメインと通信する必要に迫られた。例えば:
- www.example.com が api.example.com と通信中
- アプリがCDNまたは分析エンドポイントに接続している
- サードパーティのAPI(StripeやGoogle Mapsなど)を呼び出すWebクライアント
同一生成元ポリシーは、現代的な分散アーキテクチャを阻む障壁となった。
そこでクロスオリジンリソース共有(CORS)が登場した。
CORSはブラウザの制限を完全に撤廃するのではなく、SOP(同一オリジンポリシー)を制御された形で緩和する仕組みを導入しました。これにより、ブラウザとサーバーがドメインを跨いで安全に通信する手段が確立され、双方が合意した場合にのみ通信が許可されるようになりました。
こう考えてみてください:SOPは誰も入れない施錠されたドアです。一方CORSは同じドアですが、ゲストリストと身分証を確認するガードマンがいるのです。
柔軟性と保護のこのバランスこそが、CORS設定をあらゆる現代的なWebアプリケーションにとって極めて重要にする理由です。
同一生成元ポリシー(SOP)の理解
CORS設定についてさらに掘り下げる前に、その基盤となる「同一生成元ポリシー(SOP)」を理解することが不可欠です。
前述の通り、SOPはウェブ上の悪意ある行為に対するブラウザの第一防衛線です。これにより、あるウェブサイトが別のウェブサイトのデータを自由にアクセスすることを防ぎ、クッキーや認証トークン、個人詳細といった機密情報が漏洩するのを阻止します。
実際の動作は次の通りです:ウェブページがブラウザで読み込まれる際、プロトコル、ホスト、ポートの3要素に基づいてオリジンが割り当てられます:
https:// api.example.com :443
^ ^ ^
プロトコル ホスト ポート
次の3つの部分がすべて一致する場合に限り、2つのURLは同一オリジンと見なされます。それ以外の場合は、ブラウザはそれらをクロスオリジンとして扱います。
この単純なルールが有害なクロスサイト操作を阻止します。これがなければ、無関係なサイトがあなたのオンラインバンキング画面を非表示のフレームで読み込み、残高を読み取って攻撃者に送信する――これらすべてがあなたの同意なしに行われる可能性があります。
要するに、SOPは異なるサイト間でコンテンツを分離し、各オリジンが独立したセキュリティゾーンとなることを保証するために存在する。
なぜSOPだけでは不十分だったのか
同一生成元ポリシーは、ウェブサイトが独立していた時代には完璧に機能した。しかし、ウェブがAPI、マイクロサービス、分散アーキテクチャの生態系へと進化するにつれ、この厳格なルールは大きな制約となった。
現代のアプリケーションには以下が必要である:
- 異なるサブドメイン(app.example.com → api.example.com)でホストされている自社のAPIを呼び出す
- CDNまたはサードパーティサービスからアセットを取得する
- Stripe、Firebase、Google Mapsなどの外部APIと連携する
SOPの下では、これらの正当なクロスオリジンリクエストがブロックされました。開発者はJSONP、リバースプロキシ、ドメインの複製などあらゆる回避策を試みましたが、これらの修正策は安全性が不十分か、あるいは非常に複雑でした。
そこでCORS(クロスオリジンリソース共有)がゲームチェンジャーとなった。
CORSは、ブラウザとサーバーが信頼関係を交渉するためのハンドシェイクシステムを導入しました。単なるSOPの破棄ではなく、これを拡張することで、クロスドメイン通信において特定のオリジンを安全にホワイトリスト登録する手段を提供しました。
CORSの仕組み:プロトコルレベルの流れ
前述の通り、ブラウザが異なるオリジンにリクエストを送信する際、単に盲目的に送信するわけではありません。代わりに、明確に定義されたCORSプロトコルに従います。これは、リクエストを許可すべきかどうかを判断するための、ブラウザとサーバー間の往復のやり取りです。
CORSは本質的にHTTPヘッダーを通じて機能します。ブラウザはすべてのクロスオリジンリクエストにOriginヘッダーを添付し、リクエストの発信元をサーバーに伝えます。サーバーはこれに対し、許可内容を定義するAccess-Control-*ヘッダーを一つ以上返します。
その会話の簡略化した例は以下の通りです:
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "Success"}
この場合、サーバーは明示的にhttps://app.example.comオリジンが自身のリソースにアクセスすることを許可します。ブラウザはこの応答を確認し、一致を検証した後、データをJavaScriptに配信します。
しかし、オリジンが一致しない場合やレスポンスヘッダーが欠落・不正な場合、ブラウザは黙ってレスポンスをブロックします。データは表示されず、コンソールにイライラする「CORSエラー」メッセージが表示されるだけです。
CORS自体がサーバーのセキュリティを向上させるわけではない点に留意することが重要です。むしろ、CORSはブラウザとサーバー間の通信ルールを強制するものであり、信頼されたオリジンだけが保護されたリソースにアクセスできるようにするセキュリティ層として機能します。
CORSリクエストの種類
CORSは主に2種類のリクエストを定義します:シンプルリクエストとプリフライトリクエストです。違いは、ブラウザがデータを送信する前にどの程度の検証を行うかです。
1. 簡単なリクエスト
単純なリクエストは最もわかりやすいタイプです。特定のルールに従っている限り、ブラウザによって自動的に許可されます:
- 以下のいずれかのメソッドを使用します:GET、HEAD、またはPOST
- 特定のヘッダーのみを含みます:
- 承諾する
- Accept-Language
- コンテンツ言語
- Content-Type(ただし、application/x-www-form-urlencoded、multipart/form-data、または text/plain のみ)
- カスタムヘッダーやストリームを使用しません
これがその様子です:
# Request
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
# Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
{"message": "This is the response data"}この場合:
- ブラウザは自動的にOriginヘッダーを追加します。
- サーバーは、一致するオリジンを持つ Access-Control-Allow-Origin を返す必要があります。
- オリジンが一致しないか欠落している場合、ブラウザはレスポンスをブロックします。
2. 事前フライトリクエスト
単純でないリクエストでは状況がより複雑になります。例えば、PUTやDELETEといったメソッドを使用する場合や、Authorizationなどのカスタムヘッダーを使用する場合などが該当します。
実際のリクエストを送信する前に、ブラウザはOPTIONSリクエストを使用して事前チェックを実行します。この手順により、サーバーが意図した操作を明示的に許可していることが保証されます。
例を挙げると:
# Preflight Request
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
# Preflight Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: PUT, POST, GET, DELETE
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 3600
# Actual Request (only sent if preflight succeeds)
PUT /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer token123
{"data": "update this resource"}このシーケンスでは:
- ブラウザは非単純リクエストを検出しました。
- 事前フライトのOPTIONSリクエストを送信し、実際のメソッドとヘッダーの使用許可を求めます。
- サーバーは、許可するメソッド、ヘッダー、およびオリジンを返します。
- プリフライトチェックが成功した場合、ブラウザは実際のリクエストを送信します。そうでない場合は、リクエストをブロックします。
CORSにおける認証情報の取り扱い
クッキー、トークン、セッションベースのログインなど、認証を必要とするAPIを扱う場合、CORSの挙動は異なります。
デフォルトでは、ブラウザはセキュリティ上の理由からクロスオリジンリクエストを未認証として扱います。これは、クッキーやHTTP認証ヘッダーが自動的に含まれないことを意味します。
認証済みリクエストを安全に有効化するには、次の2つの重要な手順を整合させる必要があります:
1. クライアントは明示的に認証情報を許可する必要があります:
fetch('https://api.example.com/data', {
credentials: 'include'
})2. サーバーは明示的に許可する必要があります:
Access-Control-Allow-Credentials: true
しかし、問題がある。それも大きな問題だ。
Access-Control-Allow-Credentials が true に設定されている場合、 Access-Control-Allow-Origin でワイルドカード (*) を使用できません。使用しようとすると、ブラウザはレスポンスを拒否します。
すべてのオリジンが認証付きリクエストを送信できるようにすると、CORSセキュリティの目的そのものが無意味になるからです。これにより、インターネット上のあらゆるサイトがユーザーのセッションに関連付けられたプライベートデータにアクセスできるようになってしまいます。
だから、代わりにこれではなく:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true常に特定のオリジンを使用すべきです:
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: trueAPIが複数の信頼されたドメインに対応している場合、サーバー側で動的に正しいオリジンヘッダーを返すことができます:
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}このアプローチにより、認証済みリクエストは安全かつ意図的な状態が保たれ、試みる者すべてに開放されることはありません。
ブラウザがどのリクエストをCORS対象とするかを判断する方法
リクエストがサーバーに到達する前に、ブラウザはそれがCORSルールに該当するかどうかを判断します。
この決定は、リクエストの送信元と、それが別のドメイン、ポート、またはプロトコルを対象としているかどうかに依存します。
例えば:
- https://example.com で提供されているページから https://api.example.com をリクエスト: ✅ CORS が適用されます(異なるサブドメイン)。
- https://example.com からhttps://example.com:3000 をリクエスト中: ✅ CORS が適用されます(ポートが異なります)。
- 同じドメインとポートからの https://example.com のリクエスト: ❌ CORS は適用されません。
ブラウザがリクエストがオリジンを跨ぐことを検出すると、自動的にリクエストにOriginヘッダーを含めます:
出典: https://example.com
このヘッダーは、リクエストの発信元をサーバーに通知し、サーバーがアクセスを許可するかブロックするかを判断するために使用されます。
レスポンスに適切なヘッダー(例:Access-Control-Allow-Origin)が欠けている場合、サーバーが技術的にはレスポンスを送信したにもかかわらず、ブラウザは単にそのレスポンスへのアクセスをブロックします。
これは重要な違いです:CORSを強制するのはサーバーではなくブラウザです。
内部セキュリティチェック、XMLHttpRequestとFetchの比較、およびブラウザ間の差異
すべてのブラウザがCORSを同じ方法で処理するわけではありませんが、それらはすべて同じセキュリティモデルに従います:明示的に許可されない限り、クロスオリジンデータを決して信頼しない。
異なる点は、ルールをどの程度厳格に適用するか、そしてどのAPIに適用するかである。
1. 内部CORSセキュリティチェック
ブラウザがクロスオリジンリクエストに対するレスポンスを受信すると、JavaScriptコードに公開する前に内部的な検証ステップを実行します。
次のようなヘッダーをチェックします:
- Access-Control-Allow-Origin: リクエスト元のオリジンと一致する必要があります(または、場合によっては * である必要があります)。
- Access-Control-Allow-Credentials: クッキーまたは認証トークンが関与する場合は true である必要がある。
- Access-Control-Allow-Methods および Access-Control-Allow-Headers: 事前フライトリクエストが送信された場合、元の事前フライトリクエストと一致しなければならない。
これらのチェックのいずれかが失敗した場合、ブラウザはHTTPエラーを発生させず、単にレスポンスへのアクセスをブロックし、コンソールにCORSエラーをログします。
これによりデバッグが困難になります。実際のネットワークリクエストは成功しているにもかかわらず、ブラウザが安全のため結果を非表示にするからです。
2. XMLHttpRequest 対 Fetch
XMLHttpRequestと現代的なfetch() APIの双方がCORSをサポートしていますが、認証情報やデフォルト設定に関しては動作が若干異なります。
XMLHttpRequest を使用する場合:
- withCredentials が trueに設定されている場合、Cookie と HTTP 認証情報は自動的に送信されます。
- 事前フライトの動作は、カスタムヘッダーが追加されるかどうかによって異なります。
フェッチで:
- 認証情報(クッキー、HTTP認証)はデフォルトでは含まれません。
- 明示的に有効にする必要があります。
fetch("https://api.example.com/data", {
credentials: "include"
});- fetchはCORS下でリダイレクトをより厳密に扱う。許可されていない限り、クロスオリジンリダイレクトを追跡しないためである。
つまり、フェッチはよりクリーンでモダンですが、ヘッダーを忘れたり認証ルールを省略したりした場合の許容度が低いのです。
3. ブラウザ間の差異と癖
CORS仕様は標準化されているものの、ブラウザの実装には微妙な差異がある:
- Safariはクッキーや認証情報を含むリクエストに対して過度に厳格な場合があり、特にサードパーティのクッキーがブロックされているときに顕著です。
- Firefoxは、失敗したプリフライト応答を予想以上に長くキャッシュすることがあり、テスト中に結果に一貫性がなくなる原因となります。
- Chromeは特定のリダイレクトチェーンにおいて、他のケースよりも厳格にCORSを適用します。
これらの違いにより、あるブラウザでは完璧に動作する設定が、別のブラウザでは静かに失敗することがあります。
そのため、特に認証情報やリダイレクトが関与する場合、CORS設定をブラウザ間でテストすることが極めて重要です。
Originヘッダーのサーバーサイド処理
ブラウザがCORSを強制する一方で、実際の決定はサーバー側で行われる。
ブラウザがクロスオリジンリクエストを送信する際、常にOriginヘッダーを含めます。サーバーの役割は、このヘッダーを検査し、許可するかどうかを判断し、応答として適切なCORSヘッダーを返すことです。
1. 起源の検証
典型的なリクエストは次のように送信される場合があります:Origin: https://frontend.example.com
サーバー側では、このオリジンが許可されているかどうかをコードで確認する必要があります。最も単純(かつ安全)な方法は、信頼できるドメインの許可リストを維持することです:
const allowedOrigins = ["https://frontend.example.com", "https://admin.example.com"];
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
}これにより、既知のクライアントのみがAPIにアクセスできるようになり、その他のクライアントにはCORS許可が与えられません。
APIがクッキー、トークン、その他の認証情報を扱う場合は、Access-Control-Allow-Origin: * を返さないでください。
2. プリフライトリクエストの処理
OPTIONSプレフライトリクエストに対しては、サーバーはメインリクエストと同様の注意を払って応答しなければならない。
完全なプレフライトリスポンスには以下が含まれる:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400これらのヘッダーは、ブラウザに許可される操作と、その決定をキャッシュできる期間を伝えます。いずれかが欠落または誤っている場合、エンドポイント自体が正常に動作していても、ブラウザは後続のリクエストをブロックします。
3. CORSヘッダーの動的設定
大規模なシステム(マルチテナントプラットフォームや複数のクライアントを持つAPIなど)では、許可されたオリジンを動的に設定する必要が生じる場合があります。
例えば:
const origin = req.headers.origin;
if (origin && origin.endsWith(".trustedclient.com")) {
res.setHeader("Access-Control-Allow-Origin", origin);
}このパターンは、信頼されたドメインのすべてのサブドメインを許可しつつ、未知のソースをフィルタリングします。
送信元を慎重に検証し、制約なしにユーザー入力の文字列一致処理を行わないようにしてください。さもなければ、攻撃者が有効に見えるヘッダーを偽造する可能性があります。
4. 「Postmanでは動作する」ことが設定が正しいことを意味しない理由
CORSに関する最大の誤解の一つはこれだ:「Postmanでは動作するから、ブラウザの問題に違いない」
Postmanはブラウザではないため、CORSをまったく強制しません。
つまり、Access-Control-*ヘッダーを一切設定しない完全にオープンなAPIであっても、その環境では正常に動作するが、ChromeやFirefoxでは即座に失敗する。
PostmanではAPIが動作するものの、Webアプリでは動作しない場合、CORSヘッダーが不完全または誤って設定されている可能性が高いです。
よくあるCORS設定ミス(および回避方法)
1. 認証情報付き Access-Control-Allow-Origin: * の使用
これが最も頻繁に起こり、最も危険な間違いです。
もしあなたの回答に両方が含まれている場合:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true…ブラウザは自動的にリクエストをブロックします。
CORS仕様では、認証情報が含まれる場合、ワイルドカードの使用を禁止しています。これは、いかなるサイトでもクッキーや認証トークンに関連付けられたユーザーデータにアクセスできるようにしてしまうためです。
修正:資格情報が使用される場合、常に特定の起点(origin)を返すようにする:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true2. プリフライトリクエストの処理を忘れる
多くのAPIはGETやPOSTには正しく応答するが、OPTIONSのプリフライトリクエストを無視している。
その場合、ブラウザは実際のエンドポイントに到達せず、プリフライトの失敗後にメインリクエストをブロックします。
修正:OPTIONSリクエストを明示的に処理し、適切なヘッダーで応答する:
if (req.method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
return res.sendStatus(204);
}3. リクエストヘッダーとレスポンスヘッダーの不整合
もう一つの微妙な問題:プレフライトリクエストが特定のヘッダーを要求している場合でも、サーバーがそれらを明示的に許可していない可能性があります。
例えば、リクエストに以下が含まれる場合:
アクセス制御要求ヘッダー: 認証, コンテンツタイプ
…しかしサーバーは次のように応答するだけです:
アクセス制御許可ヘッダー: コンテンツタイプ
…ブラウザがそれをブロックします。両方のリストは完全に一致している必要があります。
修正:Access-Control-Allow-Headers に、クライアントが送信する可能性のあるすべてのヘッダー(特に Authorization、Accept、カスタムヘッダー)が含まれていることを確認してください。
4. 複数のAccess-Control-Allow-Originヘッダーの返却
一部の設定が誤ったプロキシやフレームワークは、複数のAccess-Control-Allow-Originヘッダーを送信します(例:静的な*と動的なオリジンがそれぞれ1つずつ)。
ブラウザはそれを無効と見なし、リクエストを完全にブロックします。
修正:常に単一の有効な Access-Control-Allow-Origin ヘッダーを返す。
5. 方法の制約を忘れること
Access-Control-Allow-Methods に許可されたすべてのメソッドを含めない場合、ブラウザは正当なリクエストを拒否します。
たとえば、APIがPUTをサポートしている場合でも、事前チェック応答ではGETとPOSTのみを許可している可能性があります。
修正:サポートされているすべてのメソッドを列挙するか、APIルートを動的に照合して一貫性を確保してください。
6. キャッシュされた事前フライト応答の無視
現代のブラウザはパフォーマンス向上のためプリフライトリスト結果をキャッシュします。
しかし、サーバーやCDNがオリジンによって変化させずにレスポンスをキャッシュすると、誤って別のクライアントに間違ったCORSヘッダーを送信する可能性があります。
修正:Vary: Origin ヘッダーを使用して、オリジンごとにレスポンスが個別にキャッシュされるようにします。
CORSの問題は、一つの大きなミスから生じることはほとんどありません。通常は、ブラウザの期待とサーバーの設定との間に生じる複数の小さな不一致の結果です。これらのパターンを理解することで、終わりのない「CORSエラー」のデバッグサイクルを回避できます。
CORSは敵ではない、誤解こそが敵だ
一見すると、CORSは不要な障壁のように感じられる。むしろリクエストを遮断し開発を遅らせる門番のようなものだ。
しかし実際には、これはこれまで構築された中で最も重要なブラウザのセキュリティ機能の一つです。
仕組みを理解すれば、「CORSエラー」を無作為な失敗と見なすことはなくなり、代わりにクライアントとサーバーが信頼関係、ヘッダー、認証情報についてより適切に連携する必要があるというシグナルとして捉えられるようになる。
シングルページアプリケーションを構築する場合でも、分散型APIエコシステムを構築する場合でも、CORSは安全なクロスドメイン通信を実現しつつユーザーを保護する強力な味方です。
次にあの見慣れたコンソールメッセージが表示されたら、安易な解決策に手を伸ばさないでください。ヘッダーを読み、ロジックを辿り、理解に基づいて修正を進めましょう。無作為なハックではなく、確かな理解が修正を導くのです!
今すぐソフトウェアを保護しましょう



.avif)
