2026-10-28
CORSエラーはなぜ起きるのか — 同一オリジンポリシーとプリフライトを図解する
CORSエラーは、フロントエンドを書いていれば一度は踏みます。
コンソールに赤い文字が出て、とりあえずAccess-Control-Allow-Origin: *を付けたら消える。そのまま忘れてしまう、というのがよくある流れだと思います。
ただ、このエラーが何のために出ているのかを知らないまま*を付けると、危ない場面があります。この記事では、同一オリジンポリシーから順に、CORSの仕組みを図解します。
同一オリジンポリシー
ブラウザには、あるオリジンのスクリプトが、別のオリジンのレスポンスを読めないというルールがあります。これが同一オリジンポリシーです。
https://app.example.comで動いているスクリプトから、https://api.other.exampleにfetchすると、既定ではこのルールに引っかかります。
// https://app.example.com 上のスクリプト
const res = await fetch("https://api.other.example/profile");
ブロックされるのは、レスポンスの読み取り
ここで注意したいのは、リクエストは送られていることです。
サーバーはリクエストを受け取って処理し、レスポンスも返しています。ブラウザが、そのレスポンスをスクリプトに渡さないだけです。MDNにも、CORSエラーはコンソールに出るが、サーバーが処理しないわけではない、と書かれています。
CORS — サーバーが許可を伝える仕組み
CORS(Cross-Origin Resource Sharing)は、この制限をサーバーの側から緩める仕組みです。レスポンスにヘッダを付けて、このオリジンになら読ませてよい、とブラウザに伝えます。
Access-Control-Allow-Origin: https://app.example.com
ブラウザは、リクエストに付けたOriginヘッダの値と、このヘッダを照らし合わせます。許可されていれば、スクリプトにレスポンスを渡します。
許可するオリジンは1つだけ書くか、*ですべてのオリジンを許可します。複数のオリジンを許可したい場合は、サーバー側でOriginの値を許可リストと照らし合わせて、一致したものをそのまま返します。
単純リクエストとプリフライト
CORSには、リクエストの種類によって、2通りの流れがあります。
単純リクエスト
次の条件をすべて満たすリクエストは、事前の確認なしに、そのまま送られます。
- メソッドが
GET、HEAD、POSTのどれか - 付けるヘッダが
Accept、Accept-Language、Content-Language、Content-Type、Rangeのような安全なものだけ Content-Typeがapplication/x-www-form-urlencoded、multipart/form-data、text/plainのどれか
前の図は、これにあたります。リクエストを送って、レスポンスを受け取ってから、ブラウザが許可を確認します。
プリフライト
条件を1つでも外れると、ブラウザは本番のリクエストの前に、OPTIONSメソッドで確認のリクエストを送ります。これがプリフライト1です。
たとえば、Content-Type: application/jsonでPOSTするリクエストは、Content-Typeが条件に入っていないので、プリフライトが走ります。
確認の中身は、これから送るリクエストのメソッド(Access-Control-Request-Method)と、付けるヘッダ(Access-Control-Request-Headers)です。サーバーは、許可するメソッドとヘッダを、レスポンスで返します。
許可されなかった場合、本番のリクエストは送られません。単純リクエストと違って、サーバーに届いてすらいない、というのが大きな違いです。
Access-Control-Max-Ageは、このプリフライトの結果をキャッシュしてよい秒数です。付けておくと、同じリクエストのたびにOPTIONSを送らずに済みます。
Cookieを付けるとき
別オリジンへのfetchは、既定ではCookieなどの認証情報を付けません。付けたいときは、スクリプト側で明示します。
fetch("https://api.other.example/profile", { credentials: "include" });
このとき、サーバー側にも条件が増えます。
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Credentials: trueが必要Access-Control-Allow-Originは具体的なオリジンを書く。*は使えないAllow-Methods、Allow-Headers、Expose-Headersでも*は使えない
*が使えないのには、理由があります。もし認証情報付きでも*が通るなら、どのサイトのスクリプトからでも、ログイン済みユーザーのCookie付きでAPIを呼び、その結果を読めてしまうからです。
よくある誤解
CORSは、サーバーを守る仕組みではない
CORSが制限しているのは、ブラウザの中のスクリプトがレスポンスを読むことだけです。
curlやサーバー同士の通信には、CORSは効かない- 単純リクエストは、CORSで許可されなくても、サーバーには届いて処理される
だから、CORSの設定はAPIの認可の代わりにはなりません。特に、POSTなどで状態を変える処理は、CORSが許可していなくても、リクエストは届いて実行されえます。CSRFへの備えは、CORSとは別に必要です。
CORSが守っているのは、ユーザーのブラウザが持っているログイン情報を使って、別のサイトがユーザーのデータを読み出すことです。
*を付ければ解決、とは限らない
認証情報を使わない公開APIなら、Access-Control-Allow-Origin: *で問題ありません。
一方、リクエストのOriginの値をそのままレスポンスに返す実装は、事実上すべてのオリジンを許可しているのと同じです。許可リストと照らし合わせて、一致したものだけを返します。
また、オリジンごとに値を変える場合は、Vary: Originも付けます。
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Vary: Originは、Originによってレスポンスが変わることを、キャッシュに伝えるヘッダです。付けないと、あるオリジン向けのレスポンスが、別のオリジンに使い回される可能性があります。
CORPとの違い
名前が似たヘッダにCORP(Cross-Origin-Resource-Policy)があります。どちらもクロスオリジンのリソースの扱いを決めるヘッダですが、向きが違います。
| CORS | CORP | |
|---|---|---|
| 対象のリクエスト | fetchなどでスクリプトがレスポンスを読むもの | <img>や<script>のように、CORSを使わない読み込み |
| 既定の挙動 | 許可がなければ、レスポンスを読ませない | 何も指定がなければ、読み込める |
| ヘッダの役割 | 読ませてよいオリジンを許可する | 読み込ませてよい範囲を絞る |
CORSは緩めるヘッダ、CORPは絞るヘッダ、と覚えると整理しやすいです。
参照
Footnotes
-
本番のリクエストを送る前に、サーバーが許可しているかを確認するための、
OPTIONSメソッドのリクエスト。 ↩