CORSはCSRFの対策にならないという話（あるいはCORSは何でないか）

タイトルの通りです。条件付きでなることもあります。

# CORSとは
* https://developer.mozilla.org/ja/docs/Web/HTTP/CORS
* **C**ross-**O**rigin **R**esource **S**haring
* 異なる**オリジン**でリソースを共有できるか、共有できないかを決める仕組み
  * オリジン≒ドメインだと考えてよい（厳密には [MDN](https://developer.mozilla.org/ja/docs/Glossary/Origin) あたりを参照）
* この「共有」とは「**JavaScriptから見て**」であることに注意
  * この記事ではこの話を書きます
* WebサーバーAは、Aの持つリソースをどのオリジンと共有するかをホワイトリスト形式で指定できる

# CORSが正しく機能する具体例
## 読み込まれるリソースの設定
ローカルネットワーク上に `http://example.local/secret.php` があり、GETすれば秘密の情報が取得できるとします。
また、このサーバーのレスポンスには必ず

* `Access-Control-Allow-Origin: http://good.com` 
* `Access-Control-Allow-Credentials: true`

というヘッダが付いているとしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618258/33e36f3d-4316-b44a-fb5a-ac92cc5dae14.png)


## 許可されている場合
上記の `http://example.local/secret.php` を読み込むJavaScriptを書き、`http://good.com` でホストすると、ちゃんと値が取れます。

<details><summary>HTML+JavaScript</summary><div>

```html:index.html
<!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>
```

</div></details>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618258/31feca1f-54e1-3c02-4e2b-2d7339e6d77b.png)


## 許可されていない場合
まったく同じJavaScriptを `http://evil.com` でホストすると、ちゃんと読み込みに失敗します。良かったですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618258/e8d2d5b4-89e1-d155-9767-b69021feac3e.png)

## 何が起きているのか？
次のようなことが起きています。

1. JavaScriptがXHRを生成する
2. ブラウザが**Cookieを付けてGETリクエストを送信する**
3. Webサーバー `example.local` が**GETリクエストに応じてレスポンスを返す**
4. ブラウザがレスポンスのヘッダを見てCORSに適合するかどうかチェックする
5. 適合すれば結果をJavaScriptに返し、そうでなければエラーを発生させる

`evil.com` においても、リクエスト自体は送信されてしまう（ついでに結果も返ってきている）が**JavaScriptからはその値が分からないだけ**、というのがポイントです。次に行きましょう。

# CSRFが成功してしまう例
ある脆弱なSNS、 `https://example.com` があるとします。
このサイトではログインした状態で `https://example.com/post.php` にPOSTを送ると書き込みができます。

```php:post.php
// Cookieの値を見てユーザー認証処理
...
// DBにテキストを書き込む
$db->writePost($userId, $text);
```

では次のシナリオを考えましょう。

1. ユーザーが何かのきっかけで `https://evil.com/index.html` をブラウザで開く
2. このページ上のJavaScriptがXHRを生成する
3. ブラウザが `https://example.com/post.php` へ**Cookieを付けてPOSTリクエストを送信する**
4. `post.php` が**POSTリクエストに応じて処理を行い**、レスポンスを返す
5. ブラウザがレスポンスのヘッダを見てCORSに適合するかどうかチェックする
6. 適合しないのでエラーが発生する

このJavaScriptはCSRFを発生させることが目的なので、6で発生するエラーは**どうでもいい**ですね。4のサーバー側処理で正しくCSRF対策を行っていなければアウトです。

# CSRFがCORSで防げる例
とはいえ、CORSが完全にCSRFに対して無意味だとは限りません。CORSの仕様をよく読んでみると、[プリフライトリクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Preflighted_requests)というものが出てきます。

## シンプルなクロスオリジンリクエスト
「シンプル」なクロスオリジンリクエストとは、

* 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対策とすることはできません。気をつけましょう。