CORS — 壁に穴を開けるのは「読まれる側」
プリフライトはなぜ飛ぶのか。ワイルドカードと Cookie が同時に使えないのはなぜか。エラーメッセージから原因を逆算する。
この記事の進み方
What — ブラウザとサーバーが何をやり取りするか
CORS は「読まれる側のサーバーが、読んでよいオリジンを宣言する」仕組みです。単純なリクエストと、そうでないリクエストで手順が変わります。
リクエストは普通に飛び、サーバーも普通に処理する。ブラウザは返ってきたヘッダを見て、JS に渡すかどうかを決める。サーバーは拒否していない点が重要。
問題は「サーバーの処理はすでに実行されてしまっている」ことです。GET なら読むだけなので害はありませんが、DELETE だったら? レスポンスを読めなくても、削除は実行されてしまいます。
これを防ぐために、危険になりうるリクエストには事前確認が入ります。
本番のリクエストを送る前に「送ってよいか」を OPTIONS で確認する。許可が得られなければ、本番のリクエストは一度も飛ばない。サーバーの状態は変わらない。
プリフライトが飛ぶ条件
以下をすべて満たすとき。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 エラーが出たとき、設定を直すのは主にどちら側ですか?
Why — なぜ「読まれる側」だけが許可できるのか
同一オリジンポリシーが守っているのは、読まれる側のデータです。銀行のデータを守っているのであって、攻撃者のサイトを守っているわけではありません。
もし「読む側」が許可を出せるなら、攻撃者は自分のページに「全オリジンを読んでよい」と書くだけで済みます。それでは壁の意味がありません。
だから許可を出せるのはデータを持っている側だけ。これは権限設計の一般原則と同じで、リソースの所有者だけがアクセスを許可できるという形です。
なぜ Origin ヘッダは JS から変更できないのか
fetch のオプションで headers: { Origin: 'https://bank.example.com' } と書いても、ブラウザは無視します。Origin は禁止ヘッダ名の1つで、JS からは設定できません。
もし設定できたら、攻撃者は自分を銀行だと名乗れます。サーバーは Origin を信じて許可を返し、壁が崩れます。ブラウザが付けるからこそ信頼できるヘッダです。
ワイルドカードと Cookie が両立しない理由
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 送信を同時に使えないのはなぜですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この要件を満たす 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 を挟んでいることが、レスポンスの扱いにどう影響するか
CORS エラーで丸一日止まっている後輩に、どこを見るべきかを説明してください
- 後輩はフロント側の fetch のオプションを変え続けている
- 後輩は「ブラウザがブロックしている」までは分かっている
- 同じハマり方を繰り返してほしくない
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。