← セキュリティ技術の基本
WEB PLATFORM  ·  ■ ブラウザ   ■ サーバー   ■ 攻撃者

ブラウザは、
別サイトからの返事を
読ませない。

CORS が分かりにくいのは、禁止する仕組みと、禁止を解除する仕組みが別々にあるから。土台の禁止が同一オリジンポリシー、それを部分的に開けるのが CORS です。順番に分ければ、あのエラーメッセージも読めるようになります。

0 先に結論

3つだけ先に押さえる

Rule 01

既定は「読ませない」

同一オリジンポリシー。別オリジンのレスポンスを JavaScript から読むのは禁止。

Rule 02

CORS は例外の宣言

サーバーが「このオリジンには読ませていい」とヘッダで表明する。制限ではなく緩和。

Rule 03

止めるのはブラウザ

サーバーは許可証を返すだけ。遮断はブラウザがやる。だから curl では起きない。

この3つのうち、実務で一番踏むのが Rule 03 です。「サーバーがブロックしている」と誤診すると、いつまでも直りません。

1 前提

オリジンは「3点セット」で決まる

オリジンとは、URL のうちスキーム・ホスト・ポートの3つを取り出したもの。3つ全部が一致して初めて「同一オリジン」です。パスやクエリは関係ありません。

https :// api.example.com : 443 /v1/users?id=3 SCHEME HOST PORT PATH / QUERY この3つの組み合わせが「オリジン」 ここは判定に使わない 1つでも違えば別オリジン。http と https、www あり/なし、8080 と 3000 は、すべて別物として扱われる。
Fig 1オリジンの構成要素。「同じサイトに見えるのに弾かれる」の原因は、たいていこの3つのどれかがずれている。

https://app.example.com を基準にすると、判定はこうなります。

比較する URL判定理由
https://app.example.com/other/page同一パスが違うだけ
http://app.example.com別スキームが違う
https://api.example.com別ホストが違う(サブドメインも別扱い)
https://app.example.com:8443別ポートが違う
https://example.com別ホストが違う

フロントとAPIをサブドメインで分けた瞬間に CORS の話が始まるのは、3行目が理由です。

2 土台のルール

同一オリジンポリシー(SOP)が守っているもの

ブラウザには最初から「別オリジンのレスポンスの中身を JavaScript に渡さない」という禁止が入っています。これが同一オリジンポリシー。なぜ必要かは、無かった場合を想像すると分かります。

Cookie は宛先のドメイン宛なら自動で付くという性質があります。あなたが銀行にログイン済みなら、銀行宛のリクエストにはセッション Cookie が勝手に付きます。誰がそのリクエストを出させたかは関係ありません。

evil.com のページ あなたが踏んだ罠サイト。 この中の JS が動く。 ブラウザ(門番) bank.com ログイン済みの 本物のサーバー。 ①② ③ ④ ① evil.com の JS が bank.com へ fetch する ② ブラウザが Cookie を自動で付ける → サーバーは正規の要求として処理する ③ レスポンスはブラウザまで届く(残高データも入っている) ④ SOP により JS へは渡さない ── 止まるのはここだけ
Fig 2SOP が遮断するのは「JavaScript への受け渡し」の一点。リクエストの送信もサーバーの処理も止めない。
? ここが一番の誤解ポイント

SOP は「リクエストを送らせない」仕組みではありません。返事を読ませないだけです。だから、読めなくても目的が達成できる攻撃 —— たとえば「送金 API を叩くだけでいい」というケース —— は SOP では防げません。それが CSRF です。
「CORS があるから CSRF は起きない」という説明を見かけたら、それは間違いだと思ってください。

3 解除する仕組み

CORS は、サーバーが出す「読んでいい」の許可証

ただ、SOP は厳しすぎます。自社の app.example.com から自社の api.example.com を呼ぶのも、ルール上は「別オリジン」で禁止対象。これでは困るので、サーバー側が明示的に例外を宣言する方法が用意されました。それが CORS(Cross-Origin Resource Sharing)です。

やりとりは単純です。ブラウザが「自分はどこの誰か」を Origin ヘッダで名乗り、サーバーが「そこには読ませていい」を Access-Control-Allow-Origin ヘッダで返す。ブラウザは2つを照合して、JS に渡すかどうかを決めます。

app.example.com(ブラウザ) api.example.com GET /v1/users Origin: https://app.example.com ← ブラウザが勝手に付ける。JSからは改ざんできない 200 OK Access-Control-Allow-Origin: https://app.example.com 一致した → JS にレスポンスを渡す fetch の .then() に body が届く 無い / 違う → ブラウザが握りつぶす JS からは通信失敗にしか見えない
Fig 3単純なリクエストの場合。1往復で完結する。サーバーは許可を表明するだけで、遮断そのものはブラウザが行う。
なぜ curl では起きないのか

curl や Postman には「オリジン」という概念がありません。SOP はブラウザが自分の中で守っているルールなので、ブラウザの外には存在しない。CORS エラーはサーバーが返した失敗ではなく、ブラウザが下した判断です。だから「curl だと動くのにブラウザだと動かない」は異常ではなく、当たり前の挙動です。

主なヘッダ

ヘッダ誰が送る意味
Originブラウザ「この要求はどこのページから出たか」。JS では書き換えられない
Access-Control-Allow-Originサーバー読み取りを許すオリジン。1つだけ、または *
Access-Control-Allow-Methodsサーバー許可するメソッド(プリフライトの応答で使う)
Access-Control-Allow-Headersサーバー許可する独自ヘッダ(Authorization など)
Access-Control-Allow-Credentialsサーバーtrue なら Cookie 付きの要求を許す
Access-Control-Expose-HeadersサーバーJS から読ませるレスポンスヘッダの追加指定
Access-Control-Max-Ageサーバープリフライト結果をキャッシュする秒数

Access-Control-Allow-Origin にカンマ区切りで複数書くことはできません。複数のオリジンを許したいときは、サーバー側で Origin を見て、許可リストにあればその1つだけを返す実装にします。この実装がのちに落とし穴になります(7章)。

4 2往復になるとき

プリフライト —— 送る前に「投げていい?」と聞く

DevTools を見ていて、1回のはずの通信が OPTIONS と本番の2回に見えることがあります。あれがプリフライトリクエストです。

理由は、Fig 2 の状況にあります。リクエストは送信された時点でサーバーに届いてしまうので、DELETE のように副作用のある要求は「届いた時点で手遅れ」になりかねない。そこでブラウザは、本番の前に許可を確認しにいきます。

ブラウザ api.example.com ROUND 1 — 下見 OPTIONS /v1/users/42 Access-Control-Request-Method: DELETE 204 No Content Allow-Origin / Allow-Methods: DELETE / Max-Age: 600 ROUND 2 — 本番 DELETE /v1/users/42 ← 許可が取れたので、ここで初めて実際に送る 200 OK + Access-Control-Allow-Origin ROUND 1 で許可が返らなければ、ROUND 2 は送られない。サーバーに DELETE は届かない。
Fig 4プリフライト。Max-Age が効いている間は下見を省略するので、毎回2往復になるわけではない。

プリフライトが発生する条件

次のすべてを満たすものだけが「単純なリクエスト」として1往復で済みます。1つでも外れると下見が入ります。

よくある「JSON を POST したらプリフライトが出た」は、2つ目に引っかかったケースです。Content-Type: application/json は単純リクエストの条件に入っていません。Authorization ヘッダを足した場合も同じく3つ目で外れます。

5 Cookie を伴う場合

認証付きの要求には、追加の条件がある

CORS では、デフォルトで Cookie は送られません。送るには両側の合意が要ります。

側必要な設定
ブラウザ(JS)fetch(url, { credentials: 'include' })
サーバーAccess-Control-Allow-Credentials: true
サーバーAccess-Control-Allow-Origin に * が使えない。具体的なオリジンを返す
? なぜ * が禁止なのか

* は「どのサイトから読まれても構わない」という宣言です。そこに Cookie が付くと「どのサイトからでも、ログイン済みのあなたとして読める」ことになる。Fig 2 の攻撃がそのまま成立してしまうので、仕様のレベルで組み合わせが禁止されています。

6 実務

エラーメッセージから原因を引く

コンソールの文言はほぼ定型なので、対応表を持っておくと速いです。直す場所はほぼ全部サーバー側です。

出る文言(抜粋)起きていること
No 'Access-Control-Allow-Origin' header is presentサーバーが許可ヘッダを返していない。CORS 設定そのものが入っていない
...header has a value ... that is not equal to the supplied origin許可リストに自分のオリジンが無い。末尾スラッシュや http/https 違いも多い
Response to preflight request doesn't pass access control checkOPTIONS が 401 や 404 を返している。認証やルーティングが OPTIONS を弾いている
Method DELETE is not allowed by Access-Control-Allow-Methodsメソッドの許可漏れ
Request header field authorization is not allowed by Access-Control-Allow-Headers独自ヘッダの許可漏れ
...must not be the wildcard '*' when the request's credentials mode is 'include'* と Cookie の併用。5章の禁止に抵触
切り分けの手順

まず curl -I -H "Origin: https://app.example.com" <URL> を打って、レスポンスヘッダに Access-Control-Allow-Origin が付くか見る。付いていなければサーバーの設定漏れ、付いているのにブラウザで落ちるなら値の不一致か credentials 絡み。ここで2択に割れます。

7 攻撃者の視点

設定ミスは「認証済みデータの全抜き」に直結する

CORS の設定は、緩めた瞬間に SOP という防壁に穴を開ける行為です。診断で必ず見るのが次の3つ。

① Origin をそのまま返す実装

「複数オリジンを許したい」という要求に対して、受け取った Origin をそのままエコーする実装をしてしまうケース。これに Allow-Credentials: true が組み合わさると、どのサイトからでも、被害者のセッションで API を読める状態になります。

攻撃者が試す一手
$ curl -I -H "Origin: https://evil.example" https://api.target.com/v1/me

HTTP/2 200
access-control-allow-origin: https://evil.example   # ← 送った値がそのまま返ってきた
access-control-allow-credentials: true              # ← Cookie も許可されている

# この2行が揃った時点で、罠サイトに仕込んだJSから
# 被害者のログイン状態で /v1/me を読み、中身を攻撃者サーバーへ送れる。

② null を許可している

Origin: null は、サンドボックス化した iframe やリダイレクト経由で攻撃者が意図的に作り出せます。「null なら内部からだろう」と許可リストに入れるのは危険です。

③ 文字列一致の甘さ

許可判定を部分一致や前方一致で書くと、こうなります。

判定の書き方通ってしまうオリジン
origin.endsWith("example.com")https://evil-example.com
origin.includes("example.com")https://example.com.evil.io
正規表現 /example\.com/ドットのエスケープ漏れ・アンカー漏れで同上

対策は単純で、許可リストへの完全一致にすること。動的に組み立てないのが一番安全です。

⚔ 防御側のチェックポイント

Allow-Origin が要求の反射になっていないか/null を許していないか/* と Allow-Credentials が同居していないか。
この3点は、Web 診断でも設定レビューでも最初に見るところです。リクエストの Origin を書き換えて叩き、レスポンスヘッダが追従するかを見れば一発で分かります。

+ 混同の整理

名前が似ている5つを、役割で分ける

名前何のための仕組みか誰が決める
SOP(同一オリジンポリシー)別オリジンのレスポンスを JS に読ませない、既定の壁ブラウザ(常時有効)
CORSその壁に、指定したオリジンだけ窓を開けるサーバーが宣言、ブラウザが判定
CSRF仕組みではなく攻撃の名前。意図しない要求を送らされる—
SameSite他サイト起点の要求に Cookie を付けない設定。CSRF の主対策サーバーが Cookie 属性で指定
CSP自分のページがどこから何を読み込んでよいかの制限。XSS 対策寄りサーバーがヘッダで指定

CORS と CSRF が特に混ざりやすいですが、向きが逆です。CORS は「読み取り」を許すかどうかの話、CSRF は「送信」が勝手に起きる話。読めなくても送れてしまうのが CSRF なので、CORS をどれだけ厳しくしても CSRF は防げません。

✓ 30秒でおさらい

一本の線でつなぐ

  • オリジン — スキーム・ホスト・ポートの3点セット。1つ違えば別オリジン。
  • SOP — 別オリジンのレスポンスを JS に読ませない。送信もサーバー処理も止めない。
  • CORS — サーバーが Access-Control-Allow-Origin で例外を宣言する。制限ではなく緩和。
  • 判定はブラウザ — だから curl では再現しない。エラーはサーバーの応答ではなくブラウザの判断。
  • プリフライト — 副作用のある要求は OPTIONS で下見してから送る。JSON POST や独自ヘッダで発動。
  • 危険な設定 — Origin の反射 + Allow-Credentials: true。認証済みデータが他サイトから読める。
  • CSRF は別問題 — 対策は SameSite とトークン。CORS では防げない。