CORS でつまづいた借りを返しにきた


名著[「安全なアプリケーションの作り方」](https://www.amazon.co.jp/dp/B07DVY4H3M/)をめちゃくちゃ参考にしています
より詳細には [MDN web docs の CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS) などを御覧ください
※ 網羅性を担保できてない部分もありますのでご注意ください

筆者は S3 + CloudFront のフロントに API Gateway + Lambda のバックエンドという一般的な 静的な Webアプリを作成した際に Cross-Origin Resource Sharing(以下CORS) にハマりました。
その借りを返すべく調べてみました。

# CORS の話に入る前に
Web アプリケーションのセキュリティを語る際に、当然ながら Web アプリそのもののセキュリティ対策は語られますが、実はブラウザも多くのセキュリティ対策を施してくれています。

ブラウザが実施する多くの対策のうちの一つが、**クライアントスクリプト（Javascript など）の動作の制限**です。
ブラウザは Javascript などのクライアントスクリプトの実行環境を制限することで以下のことができないようにしています

- ローカルファイルへのアクセス禁止
- プリンタなどの資源利用禁止
- ネットワークの制限（同一オリジンポリシー）

実はブラウザの制限機構により、異なるオリジン（オリジンの定義に関しては後述。いまは同じ FQDN くらいのイメージで）以外への通信は、原則できないようになっているのです。
しかし、普段から Javascript で別のオリジンへのアクセス（ex. https://example-a.com から https://example-b.com） などは行っていますよね。ここに CORS の話が関わってきます。

# 同一オリジンポリシーとはなにか
ここで「同一オリジンポリシー」とはどんな制限なのでしょうか？

同一オリジンポリシーとは
**「あるオリジンから読み込まれた文書やスクリプトについて、そのリソースから他のオリジンのリソースにアクセスできないようにする制限」**
のことです。

ここで、同一オリジンとは以下をすべて満たすものを指します

```
1. FQDN が一致している
2. スキーム(プロトコル)が一致している（HTTP, HTTPS）
3. ポート番号が一致している
```

同一オリジンの判定の具体例は[こちら](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)を参考にしてください
正確にはドメイン名ではなく、FQDN が一致している必要があります。スキームが一致しているという部分などは、恥ずかしながら筆者も調べるまで知りませんでした。


# クロスオリジンアクセスとはどういうことか
オリジンを https://example-a.com, ポート番号は皆 8080 と仮定したときに
同一オリジンか、クロスオリジンかは以下のようになります。
クロスオリジンと判定される場合は、上記の**同一オリジンポリシー**のため Javascript から原則通信できません。

| URL |   判定   | 理由 |
|:-----:|:-----:|:-----:|
| https://example-a.com/hoge/index.html  |  同一オリジン | パス違いなだけ |
| http://example-a.com  |  クロスオリジン | プロトコルが異なる  |
| https://hoge.example-a.com  |  クロスオリジン | FQDN(ホスト部分) が異なる  |
| https://example-a.com  |  クロスオリジン | FQDN(ドメイン部分) が異なる  |

表よりも情報量は落ちますがイメージ図

![CORS_クロスオリジンリソースシェアリング (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309585/1b86fd01-e0f5-4dec-789c-8f080b6cc3e6.png)


本論からはそれますが、Javascript からではなければブラウザが許可しているクロスオリジンアクセスがあります。

### クロスオリジンアクセスできる例外
詳細は「安全なWebアプリケーションの作り方」を参照してください。

- frame, iframe 要素（Javascript での クロスオリジンのドキュメントにはアクセスできない）
- img 要素の src 属性（リクエスト時に、画像のあるホストに対するクッキーがついてしまう。ステートフルなふるまいの可能性）
- script 要素の src 属性（クロスオリジンの javascript ソース取得のリクエスト時に Javascript が置いてあるサイトに対するクッキーがついてしまう。ステートフルなふるまいの可能性）
- CSS内の @import, HTML の Link 要素による CSS 取得, JS の addImport メソッドによる CSS 取得
- form 要素の action 属性

# CORS とはなにか
ここまでで「ブラウザのセキュリティ対策である**同一オリジンポリシー**によって、**クロスオリジンアクセス**は原則制限されている」ということがわかります。

しかし、現実的にはクロスオリジンで Web 上のリソースを活用したい場面はたくさんあります。
そこで作成された規格が「**CORS : Cross-Origin Ressource Sharing**」です。

CORSは、**「同一オリジンポリシーだと他のオリジンを利用できないから、特定の条件を満たすときはクロスオリジンアクセスを許可する規格」**です。
同一オリジンポリシーに依存するアプリケーションと整合性をもった上で、クロスオリジンアクセスできるように作成されています。

# CORS はどどのような条件で有効になるのか

クロスオリジンアクセスが可能になるのは、特定の条件を満たすときです。この特定の条件とはどのようなものなのか見ていきます。

まず リクエスト によって CORS に関する挙動は異なります。

それは以下の 3 つの要件をすべて満たすリクエストか、そうでないリクエスト(1つでも満たさない要件がある)かです。
ここでは要件を満たすリクエストを「単純リクエスト」そうでないものを「それ以外のリクエスト」と呼びます。
より詳細な要件の定義は[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)

```
「単純リクエストの要件」
1. メソッドが GET, HEAD, POST のいずれか
2. リクエストヘッダが Accept, Accept-Language, Content-Language, Content-Type
3. Content-Type が application/x-www-form-urlencoded, multipart/formdata, text/plain のいずれか
```

## 単純リクエストの場合

**単純リクエストの場合の挙動**

1. 【クライアント】ヘッダーに `Origin: https://example-a.com` を含めて`GET`リクエストを https://example-b.com へ送信する
2. 【サーバー】レスポンスのヘッダー`Access-Control-Allow-Origin`の設定値とリクエストの`Origin`を検証して、条件を満たせば `Status:200` とともにリクエストに対応したレスポンスを返す

例えば、サーバーの `Access-Control-Allow-Origin:https://example-a.com` と設定されている場合に、`Origin: https://example-a.com` のリクエストが来たら正常なレスポンスをクライアントに返却します。
`Access-Control-Allow-Origin: *` であればどのようなオリジンからのリクエストに対しても、正常なレスポンスを返却します。

必要なのはサーバー側で「オリジンに対する許可」を

**「単純リクエストの場合、リクエストの `Origin` がレスポンスの `Access-Control-Allow-Origin` の条件を満たしていれば、CORS を使用したクロスオリジンアクセスが可能になります」**

| CORS を使用したクロスオリジンアクセスができる条件 | リクエスト | レスポンス |
|:-:|:-:|:-:|
| オリジンに対する許可  | Origin  | Access-Control-Allow-Origin  |


## 単純ではないリクエストの場合
単純なリクエストの要件を 1 つでも満たさない場合は、クライアントとサーバーの間でやり取りが増えます。
単純なリクエストの要件ででてきた、メソッド、リクエストヘッダー、Content-Type についての検証が必要だからです。
Content-Type などの正確な取り扱いは[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)を参照してください

**単純なリクエストでない場合の挙動**

1. 【クライアント】ヘッダーの Method を `OPTIONS` にセットした上で `Origin: https://example-a.com`, `Access-Control-Request-Methods:POST(使用したいメソッド)`, `Access-Control-Request-Headers:Content-Type(使用したいヘッダー)`を含めて **プリフライトリクエスト** を https://example-b.com へ送信する
2. 【サーバー】設定されている`Access-Control-Allow-Origin:*(許可するオリジン)`, `Access-Control-Allow-Methods:POST,GET(許可するメソッド)`, `Access-Control-Allow-Headers:X-PINGOTHER, Content-Type(許可するリクエストヘッダ)`の設定値とリクエストの`Origin`を検証して、条件を満たせば正常なレスポンスを返す
3. 【クライアント】1 にのヘッダーにリクエスト内容を含めて、`POST(先程はOPTIONSだった)`メソッドでリクエストを https://example-b.com へ送信する
4. 【サーバー】レスポンスのヘッダー`Access-Control-Allow-Origin`の設定値とリクエストの`Origin`を検証して、条件を満たせば `Status:200` とともにリクエストに対応したレスポンスを返す

実際にリクエストを送る前に、プリフライトリクエストでリクエストをしても安全かどうか確認していることがわかります。

**「単純ではないリクエストの場合、プリフライトリクエストで条件を満たしていれば、CORS を使用したクロスオリジンアクセスが可能になります」**

| CORS を使用したクロスオリジンアクセスができる条件 | リクエスト | レスポンス |
|:-:|:-:|:-:|
| オリジンに対する許可  | Origin  | Access-Control-Allow-Origin  |
|メソッドに対する許可|Access-Control-Request-Methods|Access-Control-Allow-Methods|
|ヘッダに対する許可|Access-Control-Request-Headers|Access-Control-Allow-Headers|

# クロスオリジンアクセス にまつわる注意点
## 認証がからむとき
クロスオリジンアクセスではリクエストヘッダがデフォルトでは送信されません。
クッキーなどでリクエストヘッダを利用して認証している際は、クロスオリジンアクセスすると認証されません。
以下のような設定が必要になります

```
- リクエスト : withCrendential:true
- レスポンス : Access-Control-Allow-Credentials:tue
```

## プリフライトリクエストした際にリダイレクトがおこるとき
プリフライトリクエストした際に、リクエスト先のサーバーがプリフライトリクエストをリダイレクトすると、ブラウザの仕様上正しく動作しないことがあります。
対応としては主に以下の 3 つです

```
1. プリフライトが起こらないような、単純リクエストにする
2. リダイレクトが起こらないようにする
3. リダイレクト先に直接リクエストする

```
やはり詳細は[ここ](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)などをご覧ください

## リクエストに Origin が設定されないとき
リクエストに `Origin` が設定されないリクエストも存在するので注意が必要です。（通常の同一オリジン間での通信の場合は、ヘッダーに `Origin` が設定されません）
Fetch リクエストを HEAD または GET で行った場合には設定されないとのことです（ブラウザによる）

## キャッシュとクロスオリジンアクセス
CDN 等で

1. まず初回が同一オリジンからのアクセスだった場合、レスポンスに`Access-Control-Allow-Origin` が含まれずにキャッシュ
2. キャッシュされた後にクロスオリジンアクセスをした場合に、`Access-Control-Allow-Origin`ないキャッシュされたレスポンスのために正常に動作しない

ということが起こりえます。もう各 CDN 対応方法があるのでこのあたりを意識して設定しましょう
昔の記事ですがClassmethodさんの[こちらの記事](https://dev.classmethod.jp/cloud/aws/cloudfront-cross-origin-resource-sharing/)を参考にしました
キャッシュに関連してヘッダーの`Vary`は[こちら](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Vary)を参照

## Access-Control-Allow-Origin に null はだめ
`file:` などのスキームに対しては null になるように定義されているため、null はやめる

# クロスオリジンアクセス周りで問題がおきたら
クロスオリジンアクセスが絡みそうなことで問題が起きたら以下の観点をみてみると良いかもしれません。

- リクエスト
    - そもそもクロスオリジンアクセスなのか
    - Origin が設定されているか
    - 単純リクエストなのか、そうでないリクエストなのか
    - リダイレクトされていないか
    - 認証が必要なリクエストで足りないヘッダがないか
- レスポンス
    - キャッシュを見に行ってないか
    - 単純リクエスト、そうでないリクエストそれぞれで適切なヘッダーを返せているか

# 雑感
- 単にドメイン名ではなく、オリジン（FQDN・プロトコル・ポート番号）が一致しているかどうかを判定しているので、「クロスドメインアクセス」などは CORS を語る文脈ではミスリードなのではと思いました

# 参考
- [名著「安全なアプリケーションの作り方」](https://www.amazon.co.jp/dp/B07DVY4H3M/)
- [MDN web docs の CORS](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS) 
- [[新機能] Amazon CloudFrontがCORSに対応しました](https://dev.classmethod.jp/cloud/aws/cloudfront-cross-origin-resource-sharing/)
- [ドメイン名・ホスト名・FQDNの違い](https://www.geolocation.co.jp/learn/dns/01.html)