2026年10月8日にMDNのCORS本文とRFC 9110を再確認した。CORSはブラウザーが別originの応答をWebコンテンツへ公開してよいかをHTTPヘッダーとpreflightで確認する共有機構であり、攻撃者のリクエストやサーバー側の認証認可を止めるファイアウォールではない。資格情報付き要求では明示的なoriginとAccess-Control-Allow-Credentialsが必要で、*との併用はできない。サーバー側でも認証・認可、CSRF、入力検証、レート制限を実装し、CORS設定だけでAPIを保護したと判断しない。
外部のAPI(他のシステムとデータをやり取りするための窓口)をフロントエンドから呼び出したときに真っ先に遭遇するのが、CORS(Cross-Origin Resource Sharing)エラーです。多くの実装者は、このエラーを見て「サーバーがリクエストを拒否している」と考えます。そして解決策としてAccess-Control-Allow-Origin: *をサーバー側に追加し、エラーが消えたら「直った」と判断します。
この理解には見落としがあります。CORSは、攻撃者をサーバーの手前で止める壁ではありません。ブラウザが預かっている「他人のデータ」を、いま動いているスクリプトに渡してよいかどうかの宣言です。この前提を取り違えると、エラーは消えても、本来サーバー側にあるべき認可の仕組みが空いたままになっている、という事態が起こり得ます。
30秒で要点
- CORSはブラウザが応答を別オリジンのスクリプトへ共有するための仕組み。認証・認可やCSRF対策の代わりにはならない
- 必要なプリフライトに失敗すると実リクエストは送られない。一方、実リクエストが届いた後に応答の共有だけが拒否される場合もある
- 「OPTIONSと本来の要求」「HTTPの結果」「JavaScriptから読めるか」を分けて調べ、エラーを消すためだけに許可範囲を広げない
なぜ「サーバーが弾いている」と誤読してしまうのか
CORSエラーが誤解されやすいのには、仕様上の理由があります。MDNの解説にある通り、CORSの失敗はコンソールにエラーとして表示されますが、セキュリティ上の理由からその詳細情報はJavaScriptから取得できません。エラーオブジェクトをtry/catchで受け取っても、原因を示す情報はほとんど手に入らない仕様です。
情報が得られないまま目の前のエラーだけを見ると、人は「何かがブロックされた」という結果から「サーバー側で拒否された」という原因を推測しがちです。さらに、Access-Control-Allow-Originという名前自体が「アクセスを制御する」という響きを持つため、「アクセス権限の設定」=「サーバーへの入場管理」だと連想しやすくなります。この名前からの連想と、詳細情報が見えない仕様が重なって、「サーバーが弾いている」という誤読が生まれます。
確かなこと・まだ確かではないこと
WHATWG Fetch StandardのCORS protocolは、サーバーによる明示的な共有許可を定めています。イントラネット内の応答や資格情報を使って得る応答を、意図しないオリジンのスクリプトへ渡さないための制御です。 MDNのCORS解説は、フォーム相当の条件を満たす要求と、先にOPTIONSで確認する要求を区別しています。前者はCORSの共有許可がなくても送られ得るため、サーバー側には元からCSRFへの対策が必要です。ただし「必ず届く」という意味ではなく、ネットワーク障害や他のブラウザの制限は別にあります。後者では、プリフライトの応答で許可を確認できなければ、本来送りたかった要求を送らずに失敗します。「すべてのCORSエラーは到達後の話」と決めつけるのも誤りです。確認済みの仕様と、個々のブラウザやプロキシの実装結果は区別してください。この記事では実サービスへの送信やブラウザ受入は行っていません。
CORSが守っているのは「サーバー」ではなく「ブラウザの利用者」
利用者がログインした別サイトの応答を、悪意あるページのスクリプトが勝手に読むと困ります。そこでサーバーが許可するオリジンを示し、ブラウザが応答を共有するか判断します。オリジンはスキーム・ホスト・ポートの組で、URLのパスとは別です。
この役割を、サーバーへのアクセス制御と同一視してはいけません。プリフライトが送信前に作用する場合もありますが、フォーム相当の要求やブラウザ以外のクライアントを含めた認可をCORSだけで実現することはできません。認証は誰かを確認し、認可はその人が対象データを操作してよいかを確認します。CSRFは、本人の意思に反して資格情報付きの操作を行わせる脅威への別の対策です。
またHTTPの成功・失敗と共有の成功・失敗も別です。APIがHTTP 403を返していても、適切なCORSヘッダがあればJavaScriptがその403応答を読むことはあります。反対にHTTP 200でも、共有条件が合わずJavaScriptが本文を読めないことがあります。
判断軸:CORSエラーに遭遇したら確認する3つの問い
- 本来の要求は送られたか:ネットワーク記録でOPTIONSとGET/POSTなどを別行として見ます。OPTIONSだけならプリフライトの応答、実要求があればその応答を調べます。サーバーログとの照合も必要です。
- 誰に何を共有する設定か:ページのOrigin、要求のcredentials mode、Allow-OriginとAllow-Credentialsを並べます。ヘッダがあるだけでなく値の組合せを確認します。プリフライトでは要求したmethod/headerも対象です。
- 認証・認可・CSRF対策が別にあるか:共有を広げる前に、利用者とデータ単位の権限をサーバーが確かめるか確認します。CORSのエラー解消は、この確認の合格を意味しません。
架空の社内画面で条件を表にする
画面のOriginを https://app.example、APIを https://api.example とした教材です。ここでは通信障害など他の条件はないものとし、実際のAPIへは送信しません。
表が収まらない場合は、横にスクロールできます
| 条件 | 本来の要求 | JavaScriptへの応答共有 | 次に見る場所 |
|---|---|---|---|
| プリフライト不要、Allow-Originなし | 届き得る | 不可 | 実要求の応答ヘッダと権限 |
| プリフライト必要、method不許可 | 送られない | 不可 | OPTIONS応答 |
credentials omit、Allow-Originが * | 他条件を満たせば送信 | 可 | データが公開してよいものか |
credentials include、Allow-Originが *、Allow-Credentialsがtrue | 送信済みの場合もある | 不可 | wildcardを具体的な許可originへ限定できるか |
credentials include、Allow-Originが https://app.example、Allow-Credentialsがtrue | 他条件を満たせば送信 | 可 | 認可と資格情報の扱い |
includeでは、Cookieがたまたま空でもmodeに対する条件を使います。Allow-Credentialsの値は小文字のtrueで、Allow-Originのorigin表記に末尾のスラッシュを足しません。要求Originを無条件で反射する実装へ変更せず、許可集合と照合します。表は共有条件の教材であり、安全なAPIの完成判定ではありません。
自分でも試せる、最小の検証
権限を持つ検証環境のAPIを1件選び、上の表の列を埋めます。最初は既存ログや設定の読取りから始め、更新や削除を伴う要求を本番へ試しに送らないでください。
記録するのは「ページOrigin、実method、要求header、credentials mode、OPTIONSの有無と結果、実要求のHTTP結果、共有ヘッダ、サーバーの認可箇所」です。不明な欄は不明のままにし、確認担当を決めます。コンソールのCORS理由とネットワーク記録を合わせると、送信前の問題と応答共有の問題を切り分けられます。JavaScriptが受け取るエラーだけから詳細を断定しません。
判断の土台として押さえておくこと
- 共有許可は、サーバーの認証・認可・CSRF対策を代替しない
- 送信前のプリフライト失敗と、送信後の応答共有失敗を区別する
- credentials modeと許可originを組で確認し、HTTP結果と共有結果を別欄に残す
より深く学ぶ
- APIキー・シークレット管理の実務ガイド:認証情報の扱い
- モダンなWeb認証の考え方:認証方式の設計判断
参考資料・引用元
- WHATWG, "Fetch Standard — HTTP CORS protocol". https://fetch.spec.whatwg.org/#http-cors-protocol
- MDN Web Docs, "Cross-Origin Resource Sharing (CORS)". https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS