タイトルの通りです。条件付きでなることもあります。
CORSとは
- https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
- Cross-Origin Resource Sharing
- 異なるオリジンでリソースを共有できるか、共有できないかを決める仕組み
- オリジン≒ドメインだと考えてよい(厳密には MDN あたりを参照)
- この「共有」とは「JavaScriptから見て」であることに注意
- この記事ではこの話を書きます
- WebサーバーAは、Aの持つリソースをどのオリジンと共有するかをホワイトリスト形式で指定できる
CORSが正しく機能する具体例
読み込まれるリソースの設定
ローカルネットワーク上に http://example.local/secret.php
があり、GETすれば秘密の情報が取得できるとします。
また、このサーバーのレスポンスには必ず
Access-Control-Allow-Origin: http://good.com
Access-Control-Allow-Credentials: true
というヘッダが付いているとしましょう。
許可されている場合
上記の http://example.local/secret.php
を読み込むJavaScriptを書き、http://good.com
でホストすると、ちゃんと値が取れます。
HTML+JavaScript
<!doctype html>
<html>
<body>
<input type="text" id="text" style="width: 30em;" />
</body>
<script>
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
document.getElementById("text").value = xhr.responseText;
}
}
xhr.withCredentials = true;
xhr.open("GET", "http://example.local/secret.php", false);
xhr.send();
</script>
</html>
許可されていない場合
まったく同じJavaScriptを http://evil.com
でホストすると、ちゃんと読み込みに失敗します。良かったですね。
何が起きているのか?
次のようなことが起きています。
- JavaScriptがXHRを生成する
- ブラウザがCookieを付けてGETリクエストを送信する
- Webサーバー
example.local
がGETリクエストに応じてレスポンスを返す - ブラウザがレスポンスのヘッダを見てCORSに適合するかどうかチェックする
- 適合すれば結果をJavaScriptに返し、そうでなければエラーを発生させる
evil.com
においても、リクエスト自体は送信されてしまう(ついでに結果も返ってきている)がJavaScriptからはその値が分からないだけ、というのがポイントです。次に行きましょう。
CSRFが成功してしまう例
ある脆弱なSNS、 https://example.com
があるとします。
このサイトではログインした状態で https://example.com/post.php
にPOSTを送ると書き込みができます。
// Cookieの値を見てユーザー認証処理
...
// DBにテキストを書き込む
$db->writePost($userId, $text);
では次のシナリオを考えましょう。
- ユーザーが何かのきっかけで
https://evil.com/index.html
をブラウザで開く - このページ上のJavaScriptがXHRを生成する
- ブラウザが
https://example.com/post.php
へCookieを付けてPOSTリクエストを送信する -
post.php
がPOSTリクエストに応じて処理を行い、レスポンスを返す - ブラウザがレスポンスのヘッダを見てCORSに適合するかどうかチェックする
- 適合しないのでエラーが発生する
このJavaScriptはCSRFを発生させることが目的なので、6で発生するエラーはどうでもいいですね。4のサーバー側処理で正しくCSRF対策を行っていなければアウトです。
CSRFがCORSで防げる例
とはいえ、CORSが完全にCSRFに対して無意味だとは限りません。CORSの仕様をよく読んでみると、プリフライトリクエストというものが出てきます。
シンプルなクロスオリジンリクエスト
「シンプル」なクロスオリジンリクエストとは、
- GET, HEAD, POSTのいずれか
- 特定のヘッダだけを持つ
- その他いくつかの条件を満たす
という条件を全て満たすリクエストです。この場合、先ほどの例のようにいきなり対象のWebサーバーにリクエストが飛びます。
「シンプルではない」クロスオリジンリクエスト
上記の条件を満たさないリクエスト、たとえば
- DELETEリクエスト
-
X-API-KEY
ヘッダを持つリクエスト
などの場合がこれにあたります。JavaScriptが「シンプルではない」クロスオリジンリクエストを送信しようとすると、ブラウザはそれに先立ってOPTIONリクエストを送信します。
- これをプリフライト(preflight)リクエストと呼びます。
- このリクエストは強制的に発生します。この挙動はJavaScriptからは制御できません。
ブラウザはOPTIONリクエストのレスポンスで Access-Control-Allow-Origin
ヘッダ等を検証し、ここで検証が失敗すればそれ以降のリクエストはキャンセルします。問題がなければ改めて本番のリクエストを送信します(つまり1回のXHRが2回のリクエストを発生させることになります)。
具体例
つまり、次のような仕組みを採用するならCORSでCSRFを防げるということになります。
- 重要な操作を行う場合は
X-CSRFToken
のようなヘッダを付けてリクエスト送信を行うような仕様にする - このヘッダのチェックはサーバー側でも正しく行う
- サーバーでは正しく
Access-Control-Allow-Origin
ヘッダを出力する
これならば、 evil.com
上のスクリプトは
-
X-CSRFToken
ヘッダを付けてリクエストを送る- プリフライトリクエストが飛ぶのでCORSで拒否される
- もし何かの間違いがあっても、正しいヘッダの値はWebサイト側でしか知りようがないため、サーバー側処理の検証で拒否される
-
X-CSRFToken
ヘッダを付けずにリクエストを送る- サーバー側の検証で拒否される
と選択肢を潰されてしまうのでCSRF対策になります。ただし、リクエストを単純な <form>
で済ませることはできなくなります。
まとめ・CORSは何ではないか
CORSはユーザーの意図しないリクエストを発生させることを防ぐためのものではなく、返ってきた値を邪悪なJavaScriptのコードが参照することを防ぐためのものです。リクエスト自体はいくらでも発生させることができるので、サーバー側の検証なしでCSRF対策とすることはできません。気をつけましょう。