cd ~/blog/cors-explained

2026-10-28

CORSエラーはなぜ起きるのか — 同一オリジンポリシーとプリフライトを図解する

CORSHTTP同一オリジンポリシーセキュリティ

CORSエラーは、フロントエンドを書いていれば一度は踏みます。

コンソールに赤い文字が出て、とりあえずAccess-Control-Allow-Origin: *を付けたら消える。そのまま忘れてしまう、というのがよくある流れだと思います。

ただ、このエラーが何のために出ているのかを知らないまま*を付けると、危ない場面があります。この記事では、同一オリジンポリシーから順に、CORSの仕組みを図解します。

同一オリジンポリシー

ブラウザには、あるオリジンのスクリプトが、別のオリジンのレスポンスを読めないというルールがあります。これが同一オリジンポリシーです。

https://app.example.comで動いているスクリプトから、https://api.other.exampleにfetchすると、既定ではこのルールに引っかかります。

JS
// https://app.example.com 上のスクリプト
const res = await fetch("https://api.other.example/profile");

ブロックされるのは、レスポンスの読み取り

ここで注意したいのは、リクエストは送られていることです。

サーバーはリクエストを受け取って処理し、レスポンスも返しています。ブラウザが、そのレスポンスをスクリプトに渡さないだけです。MDNにも、CORSエラーはコンソールに出るが、サーバーが処理しないわけではない、と書かれています。

CORS — サーバーが許可を伝える仕組み

CORS(Cross-Origin Resource Sharing)は、この制限をサーバーの側から緩める仕組みです。レスポンスにヘッダを付けて、このオリジンになら読ませてよい、とブラウザに伝えます。

HTTP
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などの認証情報を付けません。付けたいときは、スクリプト側で明示します。

JS
fetch("https://api.other.example/profile", { credentials: "include" });

このとき、サーバー側にも条件が増えます。

HTTP
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も付けます。

HTTP
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Vary: Originは、Originによってレスポンスが変わることを、キャッシュに伝えるヘッダです。付けないと、あるオリジン向けのレスポンスが、別のオリジンに使い回される可能性があります。

CORPとの違い

名前が似たヘッダにCORP(Cross-Origin-Resource-Policy)があります。どちらもクロスオリジンのリソースの扱いを決めるヘッダですが、向きが違います。

CORSCORP
対象のリクエストfetchなどでスクリプトがレスポンスを読むもの<img>や<script>のように、CORSを使わない読み込み
既定の挙動許可がなければ、レスポンスを読ませない何も指定がなければ、読み込める
ヘッダの役割読ませてよいオリジンを許可する読み込ませてよい範囲を絞る

CORSは緩めるヘッダ、CORPは絞るヘッダ、と覚えると整理しやすいです。

参照

Footnotes

  1. 本番のリクエストを送る前に、サーバーが許可しているかを確認するための、OPTIONSメソッドのリクエスト。 ↩