仕組みから学ぶ Web
応用読了目安 14#CORS#プリフライト#オリジン#ヘッダ

CORS — 壁に穴を開けるのは「読まれる側」

プリフライトはなぜ飛ぶのか。ワイルドカードと Cookie が同時に使えないのはなぜか。エラーメッセージから原因を逆算する。

この記事の進み方

What — ブラウザとサーバーが何をやり取りするか

CORS は「読まれる側のサーバーが、読んでよいオリジンを宣言する」仕組みです。単純なリクエストと、そうでないリクエストで手順が変わります。

Figure単純なリクエストの場合
JavaScriptshop.example.comブラウザAPI サーバーapi.example.comfetch('https://api.example.com/items')GET /items + Origin: https://shop.example.com200 + データ本体Allow-Origin を確認データを渡す / エラーにする
1/5
fetch('https://api.example.com/items')JS はいつもどおり fetch を呼ぶ。CORS を意識した書き方は特に必要ない。

リクエストは普通に飛び、サーバーも普通に処理する。ブラウザは返ってきたヘッダを見て、JS に渡すかどうかを決める。サーバーは拒否していない点が重要。

問題は「サーバーの処理はすでに実行されてしまっている」ことです。GET なら読むだけなので害はありませんが、DELETE だったら? レスポンスを読めなくても、削除は実行されてしまいます。

これを防ぐために、危険になりうるリクエストには事前確認が入ります。

Figureプリフライトが飛ぶ場合
JavaScriptブラウザAPI サーバーfetch(url, { method: 'DELETE' })OPTIONS /items/1204 + Allow-Methods / Allow-Headers許可されているか判定DELETE /items/1(本番)204 + Allow-Origin結果を渡す
1/7
fetch(url, { method: 'DELETE' })DELETE は単純なリクエストの条件から外れるため、プリフライトの対象になる。

本番のリクエストを送る前に「送ってよいか」を OPTIONS で確認する。許可が得られなければ、本番のリクエストは一度も飛ばない。サーバーの状態は変わらない。

プリフライトが飛ぶ条件

Compare単純なリクエストの条件

以下をすべて満たすとき。HTML のフォームで送れる範囲、と覚えると分かりやすい。

メソッド: GET / HEAD / POST のいずれか

Content-Type:
application/x-www-form-urlencoded
multipart/form-data
text/plain
のいずれか

独自ヘッダ: なし
(Authorization や X-Request-Id も「あり」に含まれる)
往復回数
1回
サーバー処理
確認前に実行される
該当する例
画像取得・フォーム送信相当
  • この条件は「form タグや img タグで元々できたこと」と一致する。CORS 以前から可能だったので、追加の危険がない。
  • そのため事前確認を省いてよい、という理屈になっている。

すべて満たせばプリフライトなし。1つでも外れると OPTIONS が先に飛ぶ。JSON を POST するとプリフライトが飛ぶのは、Content-Type が条件から外れるため。

確認 — ここまで読めたか

CORS エラーが出たとき、設定を直すのは主にどちら側ですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ「読まれる側」だけが許可できるのか

同一オリジンポリシーが守っているのは、読まれる側のデータです。銀行のデータを守っているのであって、攻撃者のサイトを守っているわけではありません。

もし「読む側」が許可を出せるなら、攻撃者は自分のページに「全オリジンを読んでよい」と書くだけで済みます。それでは壁の意味がありません。

だから許可を出せるのはデータを持っている側だけ。これは権限設計の一般原則と同じで、リソースの所有者だけがアクセスを許可できるという形です。

なぜ Origin ヘッダは JS から変更できないのか

fetch のオプションで headers: { Origin: 'https://bank.example.com' } と書いても、ブラウザは無視します。Origin禁止ヘッダ名の1つで、JS からは設定できません。

もし設定できたら、攻撃者は自分を銀行だと名乗れます。サーバーは Origin を信じて許可を返し、壁が崩れます。ブラウザが付けるからこそ信頼できるヘッダです。

Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true を同時に指定すると、ブラウザはエラーにします。

これは意図的な制限です。* は「誰でも読んでよい」という意味。credentials: true は「Cookie を付けて送り、その結果も読んでよい」という意味。両方を許すと、世界中のどのサイトからでも、ログイン済みのあなたとしてリクエストを送り、結果を読めることになります。同一オリジンポリシーが防いでいた最悪のケースそのものです。

Cookie を使うなら、オリジンを具体的に書く必要があります。

Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

複数のオリジンを許可したい場合は、リクエストの Origin を見て動的に1つだけ返す実装にします。その際 Vary: Origin を付けないと、CDN が1つ目のオリジン向けのレスポンスを他のオリジンにも配ってしまいます。

確認 — ここまで読めたか

Access-Control-Allow-Origin: * と Cookie 送信を同時に使えないのはなぜですか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

この要件を満たす CORS 設定をどう書きますか?

与えられた条件
  • API サーバー https://api.example.com を、次の3つのフロントから使う
  • https://shop.example.com(本番)/ https://staging.example.com(検証)/ http://localhost:3000(開発)
  • 認証はセッション Cookie で行う(Authorization ヘッダは使わない)
  • 使うメソッドは GET / POST / PATCH / DELETE
  • リクエストには Content-Type: application/json と X-Request-Id を付ける
  • API は CloudFront 経由で配信されている
この軸で考える
  • · Cookie を使う構成で、ワイルドカードが使えるか
  • · 許可したいオリジンが複数あるとき、ヘッダにどう書くか
  • · CDN を挟んでいることが、レスポンスの扱いにどう影響するか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

CORS エラーで丸一日止まっている後輩に、どこを見るべきかを説明してください

与えられた条件
  • 後輩はフロント側の fetch のオプションを変え続けている
  • 後輩は「ブラウザがブロックしている」までは分かっている
  • 同じハマり方を繰り返してほしくない

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。