CORSについて過去の自分に教える

最近、CORSについて理解してきたので、昔よくわからずにコピペでCORSの設定をしていた自分に教えるつもりでCORSについて書きます。
## そもそもCORSが必要なのはなぜ？
僕がbank.comという銀行サイトを運営していたとします。
悪い人がこのbank.comに攻撃を仕掛けるためにtrap.comというサイトを作り、いろんなところへリンクを貼っていたとします。
このtrap.comはアクセスすると、一見普通のサイトですが、裏で銀行サイト（bank.com）に「xxxに1万円送金して」というリクエストを送るように仕込まれています。
### trap.com失敗する
![cors-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/08d9e7a2-3638-acbc-3f4b-bd9e2ee5a262.png)
実際にはうまくいきません。
### trap.comが成功する
だけどごくたまに

:::note warn
僕が「CORS？何それ？」という理解でbank.comを運営していた、としてください。
:::
![cors-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/9accd6f8-85ca-0a14-9e4b-c413400c01a9.png)



このようにごくたまにうまくいくことがあります。
これはbank.comへのリクエストに、bank.comへのcookieが自動で付け加えられる、という性質を悪用したためです。

こうなるとbank.comからすると
![cors-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/f76e282a-0ac9-8875-acdb-0c91f3e6cf3a.png)
というわけでCORSが出てきます。

つまり、CORSは異なるオリジンからのリクエストを制限するものです。
### ここまでのまとめ
- 異なるオリジンからのリクエストを制限したい→CORSという仕組みがある

## CORSの仕組み
上の図では「CORS？何それ？」な僕がいたのでtrap.comからbank.comへのリクエストが成功していました。
しかしちゃんと設定すると、リクエストを送ってみてもうまくいきません。多分こんなエラーが出ます
![cors-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/6f5dedd2-0e27-0d6a-82e3-88fe11d7d44c.png)

なぜでしょう？

実際は「送金して」というリクエストを送る前にブラウザがある処理をしています。
![cors-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/cd0254c9-61b8-e0d8-9803-581fc86a2d2b.png)
「送金して」というリクエストを送る前に・・・
1. ブラウザが、bank.comへのリクエストを一旦ストップする
1. ブラウザが、trap.comからbank.comへリクエストを送っていいか確認のリクエストを送る（この確認のリクエストを **プリフライリクエスト** といいます）
1. bank.comは、プリフライリクエストに対して、「このオリジンからならリクエスト送っていいよ」というレスポンスを返す
1. ブラウザが、trap.comが許可されているか確認する。許可されていなければ最初にストップしていたリクエストを送らないでエラーを出す

という処理が行われ、trap.comからの不正なリクエストをストップできます。

このままbank.com以外からのリクエスト全てをブロックしてもいいのですが、sub.bank.comというサブドメインからのリクエストは許可したい、となったらどうしたらいいでしょうか？

### CORSを設定する
sub.bank.comからのリクエストを許可する場合、CORSの設定はどこで行うか、というとサーバー側（bank.com）で行います。
![cors-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/de91c7bf-0cbd-ef64-d409-d43cad2fb44d.png)

Goでmiddelwareとして実装するとこんな感じになると思います。
```go:middleware.go
// bank.comで使用するmiddleware
func MyMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
        // https://sub.bank.comからのリクエストを許可
		w.Header().Set("Access-Control-Allow-Origin", "https://sub.bank.com")
		w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
        // リクエストのHeaderにContent-Typeを加えることを許可
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
        // プリフライリクエストはOPTIONSで送信されるので、その場合はリターンする
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}
		next(w, r)
	}
}
```
というふうに設定するとbank.comは、sub.bank.comからのリクエストを許可することができます。
### コピペでCORSの設定をしていた自分に伝えたいこと
ここで昔の自分に伝えたいことがあります。
```go:middleware.go
// おすすめしない設定
func MyMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
        // * からのリクエストを許可
		w.Header().Set("Access-Control-Allow-Origin", "*")

        // ~~~
	}
}
```
これはやめよう。
Access-Controle-Allow-Originに * を設定すると確かにエラーは出ないけど、これだと「どこからでもリクエストしてOK」ということになり危ないよ！ちゃんと許可するオリジンを指定しよう！
### ここまでのまとめ
- 異なるオリジンからのリクエストを制限したい→CORSという仕組みがある
- CORSの設定を行うと、異なるオリジンからの危ないリクエストをブロックできる（？）

:::note alert
ブロックできません
:::

## ブロックできない？
異なるオリジンからのリクエストをストップする流れはこんな感じでした。
![cors-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1036943/cd0254c9-61b8-e0d8-9803-581fc86a2d2b.png)
1. trap.comが、bank.comへリクエストを送信するよう処理する
1. ブラウザが、そのリクエストを行う前に本当にtrap.comからbank.comへリクエストを送っていいか、bank.comへ確認する（プリフライリクエスト）
1. bank.comが、プリフライリクエストに対して、許可するオリジンを示したレスポンスを返す
1. ブラウザは、bank.comからのレスポンスをもとにtrap.comが許可されていない、と判断し、trap.comからbank.comへのリクエストをやめる

これだけみると、bank.comがtrap.comを許可しなければ、trap.comはbank.comへリクエストを送ることはできないように見えますが、注意点があります。

**必ずプリフライリクエストが行われるわけではない**、という点です。

リクエストがある要件を満たす場合、単純リクエストとして扱われ、プリフライリクエストが行われません。
[オリジン間リソース共有#単純リクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#%E5%8D%98%E7%B4%94%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88)

その場合、次のような流れになります。
1. trap.comが、bank.comへリクエストAを送信するよう処理する）
1. **ブラウザが、リクエストAが単純リクエストである、と判断しリクエストAを送信する**
1. bank.comが、リクエストAを受け取り、レスポンスを返す
1. ブラウザは、bank.comからのレスポンスをもとにtrap.comが許可されていない、と判断し、bank.comからのレスポンスを表示しない

リクエスト自体は送信されてしまい、サーバーもリクエストを受け取ってしまいます。
なので「CORSの設定をしたから安心」とはなりません。
### 対策
対策としては単純リクエストの場合、使用できるヘッダーが限られています。
なのでsub.bank.comに単純リクエストでは使用できないカスタムヘッダーを使うようにし強制し、bank.com側ではカスタムヘッダーが付与されていない場合は拒否するように変更します。
そうすることで必ずプリフライリクエストが行われ、許可していないサイトからのリクエストをブロックできます。
```go:middleware.go
// カスタムヘッダーに対応
func MyMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "https://sub.bank.com")
		w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
		// カスタムヘッダーを使用するために修正
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type,X-Custom-Header")
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		// カスタムヘッダーが付与されていない場合は単純リクエストの可能性があるので拒否する
		h := r.Header.Get("X-Custom-Header")
		if h != "secret" {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		next(w, r)
	}
}
```

## まとめ
- 異なるオリジンからのリクエストを制限したい→CORSという仕組みがある
- CORSの設定を行うと、異なるオリジンからの危ないリクエストをブロックできる、わけではない。追加の設定が必要。
## 昔の自分に伝えたいこと
- Access-Control-Allow-Origin: * はやめよう
- CORSのチェックはブラウザで行われるからフロント側のコードいじっても解決しないよ
- Same-OriginとSame-Siteは違うから気をつけて
- コピペでエラー回避する前に、その設定について調べる＆自分で試すと面白いからオススメ



## 参考
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS