CORS の内部動作とプリフライト判定
別オリジン API がブラウザで弾かれる理由が腑に落ち、単純/プリフライトの分岐と Access-Control 系ヘッダの評価順、credentials とワイルドカードの制約まで設定根拠で語れるようになる。
- 1.CORS はサーバーが返す Access-Control 系レスポンスヘッダで「別オリジンからの読み取りを許す」と宣言する仕組み。ブロックするのはサーバーではなくブラウザ。
- 2.リクエストは単純(safelisted)かプリフライト要かに分岐し、後者は本番前に OPTIONS で許可を問い合わせる。判定基準はメソッド・ヘッダ・Content-Type。
- 3.credentials 付き要求では Access-Control-Allow-Origin にワイルドカード * を使えず、Origin の反射には Vary: Origin と厳密な許可リストが必須。
何を守る仕組みなのか
CORS(Cross-Origin Resource Sharing)は、ブラウザの根幹である同一オリジンポリシー(SOP)を、サーバーの許可宣言に基づいて部分的に緩和するための規格です。オリジンとは スキーム + ホスト + ポート の三つ組で、https://app.example:443 と https://api.example:443 はホストが違うので別オリジンです。
ここで最初に押さえるべき本質は、CORS はリクエストの送信そのものを止めないということです。クロスオリジンの fetch でも、リクエストは多くの場合サーバーに到達し、サーバーは処理してレスポンスを返します。ブラウザは返ってきたレスポンスを見て、「このオリジンに読ませてよい」とサーバーが明示していなければ、レスポンスを JS から読めないように遮断するだけです。つまり保護対象は「攻撃者サイトが、被害者の権限で別オリジンのレスポンス本文を盗み読むこと」であって、書き込み(副作用)を止めるのは CSRF 対策の領分です。両者を混同すると設計を誤ります。
「CORS を入れたから CSRF は防げる」は誤りです。単純リクエスト(後述)はプリフライトなしでサーバーに届き、副作用は CORS の判定前に起きてしまうことがあります。読み取りを遮断しても、送金や設定変更といった書き込み自体は実行され得ます。状態変更には CSRF トークンや SameSite Cookie を別途用意してください。
単純リクエストとプリフライトの分岐
ブラウザはクロスオリジン要求を、**プリフライト不要の「単純リクエスト(safelisted request)」**か、事前確認が要るリクエストかに振り分けます。すべての条件を満たすものだけが単純リクエストです。
| 判定軸 | 単純(プリフライト不要)の条件 | 外れるとどうなるか |
|---|---|---|
| メソッド | GET / HEAD / POST のいずれか | PUT・DELETE・PATCH 等はプリフライト必須 |
| Content-Type | text/plain・application/x-www-form-urlencoded・multipart/form-data のみ | application/json はプリフライト必須 |
| リクエストヘッダ | safelisted(Accept・Accept-Language・Content-Language 等)のみ | Authorization や独自ヘッダを足すとプリフライト必須 |
| その他 | ReadableStream を本文に使わない・XHR の upload にイベントを付けない | ストリーム送信等もプリフライト対象 |
実務で最もよく踏むのが Content-Type です。fetch で JSON を送る Content-Type: application/json は safelisted に含まれないため、それだけでプリフライトが発生します。逆に言えば、HTML の <form> が application/x-www-form-urlencoded で POST できてしまうのは単純リクエストだからで、これが CORS だけでは CSRF を防げない理由でもあります。
プリフライトの往復を読み解く
プリフライトは、本番リクエストの前にブラウザが自動で投げる OPTIONS の問い合わせです。「これからこのメソッドとこのヘッダで送りたいが、許すか」をサーバーに尋ねます。
OPTIONS /orders HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
サーバーがこれを許可するなら、対応するヘッダを返します。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
ブラウザの評価アルゴリズムは厳密です。Access-Control-Request-Method の値が Allow-Methods の集合(例 {GET, PUT, DELETE})に含まれ、かつ Access-Control-Request-Headers に挙げた各ヘッダがすべて Allow-Headers に含まれて初めて通過します。一つでも欠ければプリフライトは失敗し、本番リクエストは送られません。Access-Control-Max-Age はこのプリフライト結果をブラウザがキャッシュしてよい秒数で、同条件の連続呼び出しで OPTIONS の往復を省けます(上限はブラウザ依存で、Chromium 系は約 2 時間)。
Access-Control-Allow-Headers: * はワイルドカードとして機能しますが、Authorization だけは別扱いで、* ではカバーされません。Authorization を許可したいなら、Access-Control-Allow-Headers: *, Authorization のように明示する必要があります。credentials 付き要求では後述のとおり * 自体が無効になる点も合わせて注意してください。
credentials 付き要求とワイルドカードの制約
Cookie やクライアント証明書、Authorization ヘッダを伴う「資格情報付き要求」では、CORS の制約が一段厳しくなります。ブラウザは fetch(..., { credentials: 'include' }) のときだけ Cookie を送り、レスポンス側に Access-Control-Allow-Credentials: true が無ければレスポンスを遮断します。
決定的な制約は、credentials 付き要求では Access-Control-Allow-Origin: * が無効になることです。同様に Allow-Headers: * や Allow-Methods: * のワイルドカードも、credentials 付きではリテラルの * という文字としてしか解釈されません。許可するなら具体的なオリジン文字列をそのまま返すしかありません。
| レスポンスヘッダ | credentials なし | credentials あり(include) |
|---|---|---|
| Access-Control-Allow-Origin | * または具体オリジン | 具体オリジンのみ(* は不可) |
| Access-Control-Allow-Credentials | 不要 | true が必須 |
| Allow-Headers / Allow-Methods の * | ワイルドカードとして有効 | リテラル * 扱い(実質無効) |
| Vary: Origin | * なら任意 / 反射なら必須 | 実質必須(オリジンごとに応答が変わるため) |
* が使えないため、複数オリジンを許可したいサーバーは「リクエストの Origin を読み、許可リストに含まれていればその値をそのまま Access-Control-Allow-Origin に反射する」という実装を取ります。ここで重要なのが Vary: Origin です。レスポンスがオリジンによって変わるのに Vary を付けないと、CDN や共有キャッシュがあるオリジン向けの Allow-Origin を別オリジンに配ってしまい、キャッシュ汚染で許可が漏れます。
誤設定による情報漏洩
CORS の事故の大半は、緩和を「広げすぎる」ことで起きます。レスポンスを読めるのは攻撃者のスクリプトであり、被害者の Cookie で認証された状態なら、その応答(個人情報・トークン等)がそのまま盗まれます。
よくある危険コードは、Origin ヘッダを検証せずそのまま反射し、同時に credentials を許す実装です。
// 危険:どんなオリジンでも許可してしまう
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
これは「任意のサイトが、ログイン中の利用者の権限で API を読める」ことを意味します。* を避けたつもりが、反射によって実質「全許可かつ credentials 付き」という、ワイルドカードより危険な状態を作っています。
誤設定のパターンと対策を整理します。
| アンチパターン | なぜ危険か | 正しい対処 |
|---|---|---|
| Origin を検証せず反射 | 全オリジンに credentials 付きで読み取りを許す | 完全一致の許可リストで照合してから反射 |
| 許可判定が前方一致/部分一致 | evil-app.example や app.example.attacker.com が通る | ホスト全体を厳密一致で比較 |
| null オリジンを許可 | sandbox iframe や file:// から Origin: null を悪用される | null を許可しない |
| * と credentials の併用を反射で回避 | 実質全許可と同義 | 認証付き API は許可元を最小化 |
許可判定では部分一致を絶対に使わないことが鉄則です。origin.endsWith('app.example') のような実装は app.example.attacker.com を、origin.startsWith('https://app.example') は https://app.example.evil.com を通してしまいます。ホスト名を完全一致で照合してください。
const allowed = new Set(['https://app.example', 'https://admin.example']);
const origin = req.headers.origin;
if (origin && allowed.has(origin)) { // 完全一致のみ
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin'); // キャッシュ汚染の防止
}
- CORS が遮断するのはレスポンスの読み取りであり、リクエスト送信や副作用ではない(書き込み防止は CSRF 対策)。
- プリフライトを誘発する三条件は「非単純メソッド」「非 safelisted な Content-Type(例 application/json)」「safelisted 外のヘッダ(例 Authorization)」。
Access-Control-Allow-Origin: *は credentials 付き要求では使えず、具体オリジンの反射にはVary: Originが必須。
全体像の中での位置づけ
CORS は「ブラウザがレスポンスを読ませてよいか」を制御する一機構にすぎません。サーバーへの到達やヘッダ全般の安全設定は セキュリティヘッダ の領域であり、レスポンスを読まれた場合の被害は XSS と地続きです。一方、サーバー側が外部に対して内部リソースへリクエストを飛ばしてしまう SSRF は CORS とは別レイヤの問題で、CORS では防げません。CORS は読み取り許可の宣言、CSRF はトークンによる書き込み保護——役割を分けて、両者を重ねて設計するのが正解です。
セキュリティ Article
CORS の内部動作とプリフライト判定を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
セキュリティ
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
リクエストは単純(safelisted)かプリフライト要かに分岐し、後者は本番前に OPTIONS で許可を問い合わせる。判定基準はメソッド・ヘッダ・Content-Type。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「セキュリティ / CORS」に近いか確認する。
- 強みである「CORS はサーバーが返す Access-Control 系レスポンスヘッダで「別オリジンからの読み取りを許す」と宣言する仕組み。ブロックするのはサーバーではなくブラウザ。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。