CORS
Same Origin Policyによる読み取りの制限を、資源を持つサーバ側の明示的な許可によって緩め、別オリジンの応答をWebページのスクリプトから利用できるようにする仕組みである。サーバはAccess-Control-Allow-Originなどの応答ヘッダーで許可を示し、ブラウザは条件によって本来の要求の前にプリフライトリクエストで可否を確かめる。
イマシルちゃんの解説
Webページはスクリプトというプログラムを動かして、別のサーバからデータをもらうことがあるんだ。
ただしブラウザは、URLのスキーム(httpsなど)・ホスト名・ポート番号の組合せを「オリジン」として区別するよ。
別オリジンの応答をスクリプトに原則読ませないのが、同一オリジンポリシーという制限なんだ。
CORSは、応答するサーバの許可でこの制限の例外を作るプロトコルだよ。
許可は応答の付加情報であるHTTPヘッダーに入れて伝えるの。
ブラウザが許可を確かめることで、画面とデータを別のサーバで用意していても必要なデータを使えるようになるんだ。
この解説はわかりやすかったですか?
より深い解説
定義と位置付け
CORS(Cross-Origin Resource Sharing)は、ブラウザ上のスクリプトが別オリジンの資源を読めるかどうかを、HTTPのヘッダーのやり取りで決める仕組みである。現在はWHATWG Fetch Standardの中で「CORS protocol」として定められている。
Same Origin Policyが「別オリジンの中身は原則として読ませない」という制限であるのに対し、CORSはその例外を作るための手順である。例外を認めるかどうかを決めるのは、要求を送るページではなく、資源を持つサーバである。サーバが許可を示さなければ、ブラウザは従来どおり読み取りを止める。サーバが自ら参加を表明する、いわゆるオプトインの仕組みである。
許可を判定してスクリプトへの受け渡しを止めるのはブラウザである。サーバは許可の情報を返すだけで、要求を遮断する装置ではない。この役割分担を取り違えると、設計や障害対応を誤る。
やり取りされるヘッダー
CORSで使うヘッダーは、ブラウザが付ける要求ヘッダーと、サーバが返す応答ヘッダーに分かれる。
| ヘッダー | 付ける側 | 意味 |
|---|---|---|
| Origin | ブラウザ | 要求の元になったページのオリジン |
| Access-Control-Request-Method | ブラウザ(事前確認) | 本番の要求で使う予定のメソッド |
| Access-Control-Request-Headers | ブラウザ(事前確認) | 本番の要求に付ける予定の追加ヘッダー |
| Access-Control-Allow-Origin | サーバ | 応答を読ませてよいオリジン。1つのオリジンか * |
| Access-Control-Allow-Methods | サーバ(事前確認への応答) | 本番の要求で許すメソッド |
| Access-Control-Allow-Headers | サーバ(事前確認への応答) | 本番の要求で許すヘッダー |
| Access-Control-Allow-Credentials | サーバ | 資格情報付きの要求でも応答を読ませてよいか |
| Access-Control-Max-Age | サーバ(事前確認への応答) | 事前確認の結果をキャッシュしてよい秒数 |
| Access-Control-Expose-Headers | サーバ | スクリプトに読ませてよい追加の応答ヘッダー |
Originヘッダーの値は https://www.example.jp のように、スキーム・ホスト・ポートだけを表しパスを含まない。Access-Control-Request- で始まるヘッダーはブラウザが付けるもので、スクリプトから自由に設定できない。
事前確認なしで送られる要求
一定の条件を満たす要求は、事前確認をせずにそのまま送られる。MDNでは単純リクエスト(simple request)と呼び、Fetch Standardではこれに相当する条件を「CORS-safelisted」なメソッドとヘッダーとして定めている。
| 条件 | 内容 |
|---|---|
| メソッド | GET、HEAD、POSTのいずれか |
| スクリプトが付けるヘッダー | Accept、Accept-Language、Content-Language、Content-Typeなど、決められたものだけ |
| Content-Typeの値 | application/x-www-form-urlencoded、multipart/form-data、text/plain のいずれか |
| ヘッダーの値の長さ | 1つの値は128バイトまで、合計は1,024バイトまで |
これらはHTMLのフォームで昔から送れた種類の要求である。CORSが登場する前から別オリジンのサーバへ届いていたため、事前確認を求めなくても新しい危険は生じないという考え方による。
画面 https://www.example.jp のスクリプトが、APIサーバ https://api.example.jp からお知らせを取得する独自の例を示す。
GET /v1/news HTTP/1.1
Host: api.example.jp
Origin: https://www.example.jp
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.jp
Vary: Origin
ブラウザは応答の許可ヘッダーが自分のオリジンと一致することを確かめ、スクリプトに本文を渡す。一致しなければ、応答が届いていてもスクリプトにはエラーだけが返る。
ここで重要なのは、許可がなくても要求自体はサーバに届き、処理されている点である。MDNも、フォームの送信に見える要求をサーバが受け取るのに事前の同意は要らないと説明している。したがって、この種類の要求でデータを更新するAPIは、CORSとは別にCSRF対策を必要とする。
事前確認が必要になる条件と流れ
上の条件を外れる要求では、ブラウザは本番の要求の前にプリフライトリクエストを送る。例えば次のような要求が該当する。
- PUTやDELETEなどのメソッドを使う
- Content-Typeに
application/jsonを指定する - Authorizationヘッダーや、
X-で始まる独自のヘッダーを付ける
事前確認の要求にはOPTIONSメソッドを使う。プロフィールを更新する独自の例で流れを示す。
OPTIONS /v1/profile HTTP/1.1
Host: api.example.jp
Origin: https://www.example.jp
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.jp
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 600
Vary: Origin
- スクリプトが、JSONの本文とAuthorizationヘッダーを付けたPUTの要求を作る。
- ブラウザは条件を外れることを判断し、使う予定のメソッドとヘッダーを事前確認の要求で問い合わせる。
- サーバは、許すオリジン・メソッド・ヘッダーを応答ヘッダーで返す。
- ブラウザは応答が200〜299の状態コードであり、予定した内容がすべて許可されていることを確かめる。
- 許可されていれば本番のPUTを送る。本番の要求にも同じオリジンの許可が必要である。
許可されなければ本番の要求は送られない。単純リクエストとの大きな違いはここにある。サーバの処理を伴う要求が、許可のない相手から届く前に止まる。
事前確認の応答は、成功を表す状態コードでなければならない。Fetch Standardはこの範囲を「ok status」として定義している。事前確認の要求にも認証を求めて401を返すと、本番の要求の前に失敗する。
事前確認の結果をキャッシュする
Access-Control-Max-Ageは、事前確認の結果をブラウザが覚えておく秒数である。MDNによれば、このヘッダーがない場合の既定値は5秒である。また各ブラウザには上限があり、大きすぎる値は上限に置き換えられる。
1分ごとにPUTを送る画面を1時間使い続けた場合で、往復の回数を比べる独自の例を示す。
既定値の場合:前回の確認から既定の保持時間を過ぎているので毎回確認が必要
確認 60回 + 本番 60回 = 120往復
600秒の場合:確認は600秒ごとに1回
確認 3,600 ÷ 600 = 6回、6回 + 本番 60回 = 66往復
回線の遅延が大きい拠点からAPIを使う場合、この差は画面の反応の遅さとして表れる。ただし長くキャッシュするほど、サーバ側で許可を変更しても反映が遅れる。
資格情報付きの要求
Fetch Standardでいう資格情報(credentials)は、Cookie、TLSのクライアント証明書、HTTP認証の情報である。別オリジンへの fetch() や XMLHttpRequest は、既定ではこれらを付けない。付けるには、fetch() で credentials に include を指定するか、XMLHttpRequest の withCredentials を真にする。
ログイン中の利用者の情報を返すような応答を、ほかのサイトに読まれては困る。そのため資格情報付きの要求では、サーバの応答に厳しい条件が付く。
| 応答ヘッダー | 資格情報なし | 資格情報付き |
|---|---|---|
| Access-Control-Allow-Origin | * を使える | 具体的な1つのオリジンを返す必要がある |
| Access-Control-Allow-Credentials | 不要 | 値を true にする必要がある |
| Access-Control-Allow-Methods・Allow-Headers・Expose-Headers | * を「すべて」の意味で使える | * は「すべて」の意味にならない |
さらにAuthorizationヘッダーは、資格情報の有無にかかわらず、Allow-Headersの * だけでは許可されない。Fetch Standardはこのヘッダー名を、ワイルドカードの対象外として扱っている。
事前確認の要求自体には資格情報が付かない。資格情報付きの本番の要求を許すには、事前確認の応答にもAllow-Credentialsを含める。また、サーバがCORSで許可しても、ブラウザのサードパーティCookieの制限やCookieのSameSite属性によって、Cookieが送られないことがある。
許可の設定で起きやすい誤り
許可するオリジンが複数ある場合、サーバは要求のOriginヘッダーを許可リストと照合し、一致したものだけを応答に入れる。このとき応答の内容がOriginによって変わるため、Vary: Origin を付けて、HTTP応答をキャッシュする中継装置やブラウザが別のオリジン向けの応答を使い回さないようにする。
危険なのは、受け取ったOriginヘッダーをそのまま応答に写し、資格情報付きの読み取りまで許す設定である。これでは、どの罠のサイトからでもログイン中の利用者のデータを読めてしまう。文字列の前方一致で判定し、https://www.example.jp.attacker.example のような別のホストを通してしまう誤りもある。オリジンは完全一致で照合する。
* の許可は、誰に読まれても困らない公開データに限る。認証が必要な情報を返すAPIでは、許可するオリジンを列挙して管理する。
CORSが守らないもの
CORSはブラウザの中のスクリプトに対する制限である。curl のようなブラウザ以外のクライアントは、CORSのヘッダーを無視してAPIを呼び出せる。CORSの許可を絞っても、APIの認証や認可、入力値の検証を省略してはならない。
また、事前確認のいらない要求は許可の有無にかかわらずサーバで処理される。CORSは「読ませるかどうか」を決める仕組みであり、「要求を受け付けるかどうか」を決める仕組みではない。
スクリプトが応答を読む必要がない場合、fetch() で mode に no-cors を指定することもできる。この場合は単純リクエストの範囲の要求しか送れず、返る応答は中身を参照できない不透明な応答になる。CORSの失敗を回避する手段にはならない。
似た仕組みとの区別
Webのセキュリティに関する応答ヘッダーは名前が似ているため、誰が何を宣言するのかで区別する。
| 仕組み | 宣言する側 | 制御する内容 |
|---|---|---|
| CORS | 資源を持つサーバ | 別オリジンのページに応答の読み取りを許すか |
| CSP | ページを返すサーバ | そのページがどこからスクリプトなどを読み込み、実行してよいか |
| X-Frame-Options・frame-ancestors | 埋め込まれる側のサーバ | ほかのページへ埋め込んで表示されてよいか |
| CookieのSameSite属性 | Cookieを発行するサーバ | 別サイトからの要求にCookieを付けるか |
CORSとCSPはどちらもオリジンを列挙する設定を持つが、向きが逆である。CORSは「誰に読ませるか」を資源の側が決め、CSPは「何を読み込むか」をページの側が決める。
障害の切り分け
CORSの失敗では、セキュリティ上の理由からスクリプトはエラーの詳細を受け取れない。原因はブラウザの開発者ツールのコンソールとネットワークの記録で確認する。
| 観測した状態 | 確認すること |
|---|---|
| 正常時は読めるが、エラー時だけCORSのエラーになる | エラー応答(WAFやリバースプロキシが返すものを含む)にも許可ヘッダーが付くか |
| OPTIONSの要求が401・403・405で失敗する | OPTIONSに認証を要求していないか、OPTIONSを受け付けるか |
| 本番の要求が送られていない | 事前確認の応答のAllow-MethodsやAllow-Headersに不足がないか |
| Cookie付きの要求だけ失敗する | Allow-Originが * になっていないか、Allow-Credentialsがあるか |
| 応答は読めるが特定のヘッダーが取れない | Access-Control-Expose-Headersで公開しているか |
| 別の画面からアクセスした直後だけ失敗する | Vary: Origin がなく、中継のキャッシュが別オリジン向けの応答を返していないか |
サーバのログに要求が記録されていても、CORSが原因でないとは言えない。逆に、事前確認で止まった場合は本番の要求がログに現れない。どちらの要求がどこまで届いたかを、OPTIONSと本番の要求に分けて確認する。
午後での問われ方
- 構成図から、画面とAPIが別オリジンになるためCORSの設定が必要になる理由を説明させる
- 要求のメソッドやヘッダーから、事前確認が発生するかどうかを判断させる
- 事前確認の要求と応答に含まれるヘッダーの名前や役割を答えさせる
- Cookieを使う要求で、Allow-Originに
*を指定できない理由と必要な設定を問う - CORSを設定してもAPIの認証やCSRF対策が必要な理由を説明させる
- 応答ヘッダーの名前と、それが制御する内容の対応を区別させる