見えるセキュリティ

CORSの仕組み

CROSS-ORIGIN RESOURCE SHARING

この概念とは?

ブラウザは、別オリジンのAPIから返った応答を、APIの許可がなければページのJavaScriptに読ませません。CORSは、APIが応答ヘッダーで「このオリジンのページには読ませてよい」と伝える仕組みです。ブラウザがその許可を確認すると、ページのJavaScriptが応答を読めるようになります。

THEME
web-vuln
KIND
mechanism
REVIEW
self-reviewed ·

オリジンとは?

オリジンは、URLの「スキーム・ホスト・ポート」の組み合わせです。

商品ページのURLを分けて見る

https://app.example:443/products

スキーム
https

通信の方式

ホスト
app.example

接続先の名前

ポート
443

通信の窓口となる番号

/products はページの場所を示すパスで、オリジンには含まれません。HTTPSの標準ポートは443なので、:443 は省略できます。

通常のHTTP(S)では、3要素すべてが同じなら同一オリジンで、1つでも異なれば別オリジンです。

  • パスだけが違う

    https://app.example/products

    https://app.example/cart

    同一オリジン。スキーム・ホスト・ポートが同じ

  • ホストが違う

    https://app.example/products

    https://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円

  1. STEP 1JavaScriptが商品情報を要求する

    JavaScriptが取得を依頼

    ブラウザがAPIへ送信

    GET · 商品情報を要求

    APIへ届く

この図について

表示は概念図です。実際の通信やコードの実行は行いません。

STEP 01 / 4

JavaScriptが商品情報を要求する

STEP 1 / 4: JavaScriptが商品情報を要求する

現在のSTEP 1STEP 2STEP 3STEP 4

商品ページのJavaScriptが、別オリジンの商品APIに商品情報を要求します。

技術的な詳細を見る

https://app.example と https://api.example はホストが異なるため、別オリジンです。ブラウザは要求に Origin: https://app.example を付けます。この例は mode: "cors"・credentials: "omit" のGETで、独自の要求ヘッダーや本文を使わないため、プリフライトは不要です。

パネルの開閉ではSTEPは変わりません。

分かったことを確認

この例では、APIは両方のモードで商品情報を返します。ブラウザがAPIの読み取り許可を確認し、許可がある場合に商品ページのJavaScriptがその情報を読めます。

理解確認

別オリジンの商品情報を読むとき、読み取りの許可を示す側と、その許可を確認する側はどれですか?

CORSの補足

許可はどう伝える?

許可は、APIの応答ヘッダーで伝える

この図でAPIの設定を変える

商品API

APIの応答

応答ヘッダー
このページへの許可の記載なし
本文
商品情報:ノート・300円
APIからブラウザへ返るHTTP応答読み取り許可の有無にかかわらず、応答ヘッダーと本文が1つの応答としてブラウザへ届きます。

ブラウザ

商品ページのオリジン

https://app.example

許可先の記載がない

ブラウザ内でJavaScriptへの公開を止めるブラウザ内の処理を表し、追加のHTTP通信ではありません。ブラウザ内の処理

商品ページの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の設定を変える

ブラウザ

商品API

  1. ① 事前確認(OPTIONS)

    ブラウザから商品APIへOPTIONSこのページのオリジンと、使いたいPUTメソッドを伝えて事前確認します。
  2. ② APIの応答 · HTTP 204

    必要な許可の記載なし

    商品APIからブラウザへ事前確認の応答応答は返りますが、必要な許可を示すヘッダーはありません。
  3. ③ ブラウザが送信を止める

    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: PUT

APIからの事前確認への応答

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です。既定ポートの明示・省略や、パスだけの違いでは別オリジンになりません。

比較元:https://app.example/products
比較するURLOriginの判定理由
https://app.example/help同じパスだけが違う
https://api.example/products別ホストが違う
http://app.example/products別スキームが違う
https://app.example:8443/products別ポートが違う

この表はOriginの判定例であり、各URLへの通信が必ず許可されることを示すものではありません。

参考資料

RELATED CONCEPTS