見えるセキュリティ
CORSの仕組み
CROSS-ORIGIN RESOURCE SHARING
この概念とは?
ブラウザは、別オリジンのAPIから返った応答を、APIの許可がなければページのJavaScriptに読ませません。CORSは、APIが応答ヘッダーで「このオリジンのページには読ませてよい」と伝える仕組みです。ブラウザがその許可を確認すると、ページのJavaScriptが応答を読めるようになります。
- THEME
- web-vuln
- KIND
- mechanism
- REVIEW
- self-reviewed ·
オリジンとは?
オリジンは、URLの「スキーム・ホスト・ポート」の組み合わせです。
https://app.example:443/products
- スキーム
https通信の方式
- ホスト
app.example接続先の名前
- ポート
443通信の窓口となる番号
/products はページの場所を示すパスで、オリジンには含まれません。HTTPSの標準ポートは443なので、:443 は省略できます。
通常のHTTP(S)では、3要素すべてが同じなら同一オリジンで、1つでも異なれば別オリジンです。
パスだけが違う
https://app.example/productshttps://app.example/cart同一オリジン。スキーム・ホスト・ポートが同じ
ホストが違う
https://app.example/productshttps://api.example/products別オリジン。ホストが異なる
ブラウザはSOP(同一オリジンポリシー)により、ページのJavaScriptが別オリジンの応答を自由に読むことを制限します。CORSは、APIが許可したオリジンのページに、その応答の読み取りを認めるための仕組みです。
同一オリジンから取得する場合は、CORSの追加許可は不要です。ログインやAPI独自の権限の確認とは別の仕組みです。
この例では、どちらのモードでもAPIは商品情報を返します。比較するのは、その応答を商品ページのJavaScriptが読めるかです。
事前確認(プリフライト)が不要なGETの例です。
商品ページで、別オリジンの商品情報を表示したい
APIからこのページへの許可は示されず、ブラウザはJavaScriptに商品情報を読ませません。
図の読み方
時間は上から下へ ↓ /「次へ」で1 STEPずつ表示
ブラウザ
ページ:https://app.example
商品ページのJavaScript
ブラウザのCORS確認
商品API(接続先サーバー)
https://api.example
商品情報:ノート・300円
STEP 1JavaScriptが商品情報を要求する
JavaScriptが取得を依頼
ブラウザがAPIへ送信
GET · 商品情報を要求
APIへ届く
STEP 01 / 4
JavaScriptが商品情報を要求する
STEP 1 / 4: JavaScriptが商品情報を要求する
商品ページのJavaScriptが、別オリジンの商品APIに商品情報を要求します。
技術的な詳細を見る
https://app.example と https://api.example はホストが異なるため、別オリジンです。ブラウザは要求に Origin: https://app.example を付けます。この例は mode: "cors"・credentials: "omit" のGETで、独自の要求ヘッダーや本文を使わないため、プリフライトは不要です。
分かったことを確認
この例では、APIは両方のモードで商品情報を返します。ブラウザがAPIの読み取り許可を確認し、許可がある場合に商品ページのJavaScriptがその情報を読めます。
理解確認
CORSの補足
許可はどう伝える?
許可は、APIの応答ヘッダーで伝える
商品API
APIの応答
- 応答ヘッダー
- このページへの許可の記載なし
- 本文
- 商品情報:ノート・300円
ブラウザ
商品ページのオリジン
https://app.example
許可先の記載がない
商品ページのJavaScript
JavaScriptは読めない
APIの応答にこのページへの許可がないため、ブラウザはJavaScriptに応答を読ませません。
通信条件とヘッダーを見る
本編と同じ、独自要求ヘッダー・本文なしのGET、mode: "cors"、credentials: "omit"の例です。両条件ともAPIはHTTP 200と同じ商品情報を返します。表示上のシミュレーションで、実際の通信は行いません。
Origin: https://app.exampleはブラウザが要求に付けます。APIの許可は応答ヘッダーの一部です。
HTTP/1.1 200 OK
Content-Type: application/json
{"name":"ノート","price":300}API側で許可するオリジンを固定値として設定することもできます。ページのJavaScriptが許可を発行するものではありません。
ブラウザは応答ヘッダーを受信した時点で許可を確認できます。応答本文をすべて受信し終えることは、CORS検査の前提ではありません。JavaScriptへの公開はブラウザ内部の処理です。
- API側で共有先を決め、Originを無条件で反射しないようにします。資格情報を使わない公開リソースではAccess-Control-Allow-Origin: *が適切な場合もあり、一律に危険というわけではありません。
- Access-Control-Allow-Originに複数のOriginをカンマで並べることはできません。要求元に応じたOriginを返す場合は、Vary: Originで応答の違いを示します。
送る前に確認する場合
PUTを送る前の確認
ブラウザ
商品API
① 事前確認(OPTIONS)
② APIの応答 · HTTP 204
必要な許可の記載なし
③ ブラウザが送信を止める
PUTは送らない
事前確認でAPIの許可を確認できないため、ブラウザは本来のPUTを送りません。
通信条件とヘッダーを見る
本編のGETとは別の模式例です。ページはhttps://app.example、取得先はhttps://api.example/products/1。要求本文・独自要求ヘッダーなしのPUTを、mode: "cors"、credentials: "omit"で送ります。有効なプリフライトキャッシュはないものとします。
PUTというメソッドのため、ブラウザが先にOPTIONS要求を自動的に送ります。アプリが手動で送る手順ではありません。
ブラウザからの事前確認
OPTIONS /products/1 HTTP/1.1
Host: api.example
Origin: https://app.example
Access-Control-Request-Method: PUTAPIからの事前確認への応答
HTTP/1.1 204 No Content両条件ともOPTIONSへの応答はHTTP 204です。許可なしでは必要な許可ヘッダーがありません。許可ありでは、CORSの許可先とPUTの許可の両方を確認します。独自要求ヘッダーはないため、要求・応答の許可ヘッダー一覧は使いません。
事前確認が通っても、PUTの処理成功や応答の読み取り成功を保証しません。実際のPUTへの応答にもCORSの確認が必要です。送信後のCORS検査に失敗しても、送信済みの要求を取り消すわけではありません。
- GETでも独自の要求ヘッダーなどによってプリフライトが必要です。GET以外でも、条件を満たすPOSTなどはプリフライトが不要です。メソッドと要求ヘッダーがCORS-safelistedの条件などを満たすかで決まります。
- 応答のContent-Typeがapplication/jsonであることだけでは、事前確認が必要になる理由になりません。プリフライトキャッシュを再利用できる場合は、毎回OPTIONSが見えるとは限りません。
- プリフライトの許可は、ログインやAPIの認証・認可の代わりにはなりません。
ログイン情報を伴う場合
- Cookie付き通信
- 基本の図解はcredentials: "omit"の例です。fetchの既定値はsame-originです。別OriginへのfetchでCookieなどを含めるにはcredentialsの設定を考慮します。includeであっても、Cookie属性やブラウザの第三者Cookie制限は別に適用されます。
- 資格情報を含む場合の共有許可
- credentialsがincludeの場合、応答の共有には具体的なOriginとAccess-Control-Allow-Credentials: trueが必要です。Access-Control-Allow-Originの*は使えません。実際にCookieが付いたかどうかだけで判定するものではありません。
- 資格情報を使わない公開リソース
- 基本の図解は、追加の独自リクエストヘッダーや本文を持たないGETを、mode: "cors"、credentials: "omit"で送る例です。APIはどちらの比較でも200応答を返す設定です。
CORSで防げること・防げないこと
- 開発者ツールとJavaScriptの違い
- 開発者ツールに通信や応答が表示されても、それをページのJavaScriptが読み取れるとは限りません。
- no-corsとCORSエラーの違い
- 別Originへのfetchでmode: "no-cors"を使うと応答はopaqueとなり、JavaScriptから本文などを読めません。JSONを読めるようにする解決策ではありません。通常のCORS検査失敗でfetchのPromiseがrejectされ、読み取れるResponseがJavaScriptに渡らない場合とも区別します。
- CSRFとの違い
- CORSで内容を読めなくても、送信済みの要求が取り消されるわけではありません。状態を変更する処理では、CSRF対策などを別途検討します。
- 認証・認可との役割分担
- CORSはブラウザへの共有方針であり、APIの認証・認可を代替しません。APIが独自の認証・認可やOrigin確認により403などで拒否する処理は、別に実装できます。CORS不許可が常に403や応答なしを意味するわけではありません。サーバー間通信などのアクセスまで、CORSヘッダーだけで制御することはできません。
オリジンを詳しく見る
通常のHTTP(S) URLでは、スキーム・ホスト・ポートの組み合わせを比べます。ポートを省略した場合の既定値はHTTPが80、HTTPSが443です。既定ポートの明示・省略や、パスだけの違いでは別オリジンになりません。
| 比較するURL | Originの判定 | 理由 |
|---|---|---|
https://app.example/help | 同じ | パスだけが違う |
https://api.example/products | 別 | ホストが違う |
http://app.example/products | 別 | スキームが違う |
https://app.example:8443/products | 別 | ポートが違う |
この表はOriginの判定例であり、各URLへの通信が必ず許可されることを示すものではありません。
参考資料
- 参考資料MDN — Cross-Origin Resource Sharing (CORS)
CORSの全体像・プリフライトが不要な要求
- 参考資料MDN — Same-origin policy
Originの判定・読み取りと書き込み等の区別
- 参考資料WHATWG — Fetch Standard
CORS検査・HTTP fetch・プリフライトの処理
- 参考資料MDN — Origin
ブラウザが付ける要求元Origin
- 参考資料MDN — Access-Control-Allow-Origin
応答の共有許可・Originの指定・Vary
- 参考資料MDN — Preflight request
OPTIONSによる事前確認とキャッシュ
- 参考資料MDN — Access-Control-Allow-Methods
事前確認で許可するメソッド
- 参考資料MDN — Access-Control-Allow-Headers
事前確認で許可する要求ヘッダー
- 参考資料MDN — CORS-safelisted request header
要求のContent-Typeなどの条件
- 参考資料MDN — Request.credentials
omit・same-origin・includeと既定値
- 参考資料MDN — Access-Control-Allow-Credentials
資格情報を使う場合の共有許可
- 参考資料MDN — Using the Fetch API
応答処理・Cookieの送信条件
- 参考資料MDN — Response.type
CORSの失敗とopaque応答の区別
- 参考資料MDN — CORS configuration
共有先を決める方針・ワイルドカード
- OWASPOWASP — REST Security Cheat Sheet
APIの認証・認可とCORSの役割分担