CORSについて試してみた

# 背景・目的
JavaScriptを利用した複数オリジンへのアクセスを調べる機会がありましたので、整理します。
ここでは、複数オリジンの定義や登場した背景やどのように設定するかをまとめます。

# まとめ
Webアプリケーションの前提として、同一オリジンポリシーが基本としてあります。しかし、Ajaxなどで別オリジンへのアクセスを行う場合には、CORSヘッダーによる制御が必要になります。
CORSの仕組みでは、単純リクエストとプリフライトリクエストにより挙動が変わります。
プリフライトでは、事前にお伺いを立てて確認します。それ以降は単純リクエストと同じ挙動です。


# 概要
## CORSとは？
- Cross-origin resource sharingの略。
- [mdn](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)のドキュメントによると、以下のように書かれていました。
> オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。

- 異なるオリジンへのアクセス時にHTTPヘッダーによる制御が必要。
- 同じオリジンへのアクセス時には、不要。

### そもそも、Originとは？
- [mdn](https://developer.mozilla.org/ja/docs/Glossary/Origin)のドキュメントによると、以下のように書かれていました。

> ウェブコンテンツのオリジンOriginは、ウェブコンテンツにアクセスするために使われる URL のスキーム (プロトコル)、 ホスト (ドメイン)、 ポート によって定義されます。スキーム、ホスト、ポートがすべて一致した場合のみ、二つのオブジェクトは同じオリジンであると言えます。

- URLスキーム、ドメイン、ポートの組み合わせのこと。これを同一オリジンポリシーという。
- これらが全て一致したときに、同一オリジンとして判別される。
- 以下に、例を記載します。

|結果| ドメイン| 理由| 
|:--|:--- |:--- | 
|同一オリジン|```http://example.com/app1/index.html```<BR>```http://example.com/app2/index.html``` | スキーム (http) およびホスト (example.com) が同じなので同一オリジン| 
| |```http://Example.com:80```<BR>```http://example.com```| サーバーは既定で80番ポートで HTTP コンテンツを配信するため、同一オリジン|
| 異なるオリジン| ```http://example.com/app1```<BR> ```https://example.com/app2```|スキームが異なる|
||```http://example.com```<BR>```http://www.example.com```<BR>```http://myapp.example.com```|ホストが異なる|
| |```http://example.com```<BR>```http://example.com:8080``` |ポートが異なる |


## なぜ、異なるオリジンの場合にCORSが必要なのか。

> セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP リクエストを制限しています。例えば、 XMLHttpRequestや Fetch API は同一オリジンポリシー (same-origin policy) に従います。つまり、これらの API を使用するウェブアプリケーションは、そのアプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。

- セキュリティ上の理由でブラウザは、スクリプトによるオリジン間HTTPリクエストを制限しており、JavaScriptのXMLHttpRequests、FetchなどのAPIでは、同一オリジンポリシーに従っているとのことです。
- 同一オリジンポリシーにより、セキュリティは守られるが、Ajaxなどの通信を利用した場合など柔軟性に乏しい。
- Web通信の前提として、同一オリジン接続、異なるオリジンにアクセスする場合はCORSにより通信を行う必要がある。


### セキュリティ上どのような懸念があるのか？
- XSSやCSRFがあげられる。

#### XSSとは？
- cross site scriptingの略。
- トレンドマイクロさんの「[XSS](https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/xss.html)」には、以下のように書かれていました。

https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html
> ユーザのアクセス時に表示内容が生成される「動的Webページ」の脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。

#### CSRFとは？
- cross-site request forgeriesの略。
> クロスサイトリクエストフォージェリ（CSRF）とは、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。

## クロスオリジンアクセスを行うには？
- HTTPヘッダーとHTTPレスポンスヘッダーで制御するが、[アクセス制御シナリオの例](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1%E3%82%B7%E3%83%8A%E3%83%AA%E3%82%AA%E3%81%AE%E4%BE%8B)によると、単純リクエストとプリフライトリクエストの2種類により、制御方法が変わる。
- 単純リクエストとプリフライトリクエストの違いは、以下の全ての条件に該当する場合、単純リクエストになりそれ以外がプリフライトリクエストになります。

|項目 |条件 | 備考|
|:-- |:-- |:-- |
|メソッド |GET,HEAD,POSTのいづれか | |
|設定できるリクエストヘッダー |Accept、Accept-Language、Content-Language、Content-Typeのいづれか | Content-Typeは下記の条件を満たすもの。|
|設定できるContent-Type | application/x-www-form-urlencoded、multipart/form-data、text/plainのいづれか |


### 単純リクエストによる制御について
- 上記のとおり単純リクエストによるHTTPリクエストの場合、HTTPリクエストに設定されたオリジンと、リクエスト受け付けるサーバで許可しているオリジン（Access-control-origin）が一致するかを判断し、レスポンスが受け取れるか決まります。

#### Access-control-originが一致する場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/1a37f986-9367-7d92-c85c-999590ca36bf.png)


#### Access-control-originが一致しない場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f8a6d4eb-c8a0-39da-7c2d-1b2dcc74ee51.png)


### プリフライトリクエストによる制御について
- 単純リクエストによるHTTPリクエストと判断されなかった場合、ブラウザは自動で通常リクエストの前に、OPTIONメソッドでお伺いを立てます。これをプリフライトリクエストといいます。
- お伺い後、許可された後は、単純リクエストと同様です。拒否後はそこで終了になります。

#### プリフライトリクエストが許可された場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/939d6a8c-dda8-3576-424a-e2e6d4f0babe.png)


#### プリフライトリクエストが拒否された場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/a390b8bc-6aa4-9783-2cc3-12afde6f5797.png)

# 実践
- 実際に手を動かして、CORSの挙動を理解してみようと思います。
- とにかくWebアプリケーションを用意する必要がありますので、AWSの「[基本的なウェブアプリケーションを構築する](https://aws.amazon.com/jp/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/)」を参考に環境を構築したいと思います。

## 事前準備
### 1.Webアプリケーション 
#### コードの準備
- 以下のコードを,index.htmlファイルとして保存し、zip圧縮します。
```
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>

<body>
    Hello World
</body>
</html>
```

#### Amplifyトップページ
- Amplifyのマネコンで「使用を開始する」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/042f8e56-2dee-eca3-3ff9-a28f9f8f71c0.png)

- ウェブアプリケーションをホストで「使用を開始する」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/61d8b3f4-993b-d333-9e59-d536eb37cc9a.png)

#### デプロイ
- 「Gitプロバイダーなしでデプロイ」を選択し、「続行」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5f250e0d-82ae-437f-2e92-11fa0bfae29b.png)

- 以下を入力し、保存してデプロイをクリックします。
    - アプリケーションの名前：GettingStarted
    - 環境名：dev
    - 方法：ドラッグアンドドロップ
    - ファイルは、index.html.zipを選択。（コードの準備で作成し、zip圧縮したもの。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5a82deed-7182-be8d-d490-27996db211b6.png)

- しばらくするとデプロイが完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6ea119f5-106b-0092-3095-055b66aeabda.png)

### 確認
- リンクをクリックすると確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/a138a8f7-2e2a-e813-1c61-c2852eca013f.png)



### 2.サーバレス関数を作成する
#### Lambdaトップページ
- Lambdaのマネコンで「関数の作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d80c2192-ab98-71c2-1bf0-9e9c65658f51.png)

- 以下を入力し、関数の作成をクリックします。
    - 関数名：HelloWorldFunction
    - ランタイム：Python3.8
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/7d99c1c2-a4f4-f559-bae7-04962257d5ab.png)

#### コード
- 以下のコードを貼り付けて、「Deploy」をクリックします。
```
# import the JSON utility package since we will be working with a JSON object
import json
# define the handler function that the Lambda service will use an entry point
def lambda_handler(event, context):
# extract values from the event object we got from the Lambda service
    name = event['firstName'] +' '+ event['lastName']
# return a properly formatted JSON object
    return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda, ' + name)
    }

```

#### 確認
- Testボタンをクリックします。
- 以下を入力し、「保存」をクリックします。
    - イベント名：HelloWorldTestEvent
    - イベントJSONに以下を入力
```
{
"firstName": "Ada",
"lastName": "Lovelace"
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/cf1d56fe-585e-8218-b6cd-76b462fb1c34.png)

- Testボタンをクリックし、以下が表示されました。
    - 上記で作成したLambdaは、firstNameとlastNameから値を取得し、JSONフォーマットのbody属性に埋め込むコードでした。
    - 結果を見ると、HelloWorldTestEventで引数に渡した、AdaとLovelaceが表示されてるので成功ですね。
```
Test Event Name
HelloWorldTestEvent

Response
{
  "statusCode": 200,
  "body": "\"Hello from Lambda, Ada Lovelace\""
}

Function Logs
START RequestId: 614a7680-a599-475a-9379-7f7ba35e702f Version: $LATEST
END RequestId: 614a7680-a599-475a-9379-7f7ba35e702f
REPORT RequestId: 614a7680-a599-475a-9379-7f7ba35e702f	Duration: 1.38 ms	Billed Duration: 2 ms	Memory Size: 128 MB	Max Memory Used: 37 MB	Init Duration: 138.99 ms

Request ID
614a7680-a599-475a-9379-7f7ba35e702f
```
### 3.サーバーレス関数をウェブアプリケーションにリンクする
- API Gatewayを作成し、Lambda関数とつなぎます。

#### 新しいREST APIを作成
- API GatewayのマネコンのREST APIで「構築」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/4f335459-5a77-dbbc-30fd-e1c605509f1f.png)

- 以下を入力し、「APIの作成」をクリックします。
    - プロトコル：REST
    - 新しいAPI
    - API名：HelloWorldAPI
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2ecda6aa-9d09-803b-8371-71bc94c116a2.png)

#### 新しいリソースとメソッドを作成
-  メソッドの作成をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f6178f40-b26d-89bc-a2fc-75c1b6bd6db7.png)

- POSTを選択し、チェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/09bb1156-4907-4b81-5ef6-c221e314f3a5.png)

- 以下を入力し、保存をクリックします。
    - 統合タイプ：Lambda関数
    - Lambda関数：HelloWorldFunction（2.で作成したLambda関数）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d958cfc1-71eb-23d5-46a7-86f69d679ae4.png)

- 「API Gateway に、Lambda 関数を呼び出す権限を与えようとしています:」と表示されるので、OKをクリックします。
- POSTメソッドを選択した状態で、アクション＞CORSの有効化をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/ed8a20e6-89dd-8627-91f0-2fd16cc73738.png)
- メソッドにPOSTが選択されていることを確認し、「CORSを有効にして既存のCORSヘッダーを置換」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/6dfd2d9e-61e3-ee54-13be-ceb39e97e056.png)
- メソッド変更の確認で「はい、既存の値を置き換えます」をクリックします。

#### APIのデプロイ
- アクション＞APIのデプロイをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/920e6159-58fe-a3df-5096-eab5a40c468d.png)
- 以下を入力し、デプロイをクリックします。
    - デプロイされるステージ：[新しいステージ]
    - ステージ名:dev
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0b46b726-3d8b-d57d-6bb8-c42e0ee7cbe4.png)

- 「変更を保存」をクリックします。

#### APIの検証
- ナビゲーションペインの「リソース」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b60f6807-8e03-9ca3-ad35-91c22bbb861f.png)

- POSTをクリックし、テストをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/4b97f687-dd35-a10c-0b83-953d7e704e61.png)

- 以下をリクエスト本文フィールドに貼り付けて、テストをクリックします。
```
{
    "firstName":"Grace",
    "lastName":"Hopper"
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/5e569240-910f-7a1d-98fb-e61c2d2a000f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0a1a84a6-4b6a-3355-4a06-158a5221f632.png)

- リクエスト本文に設定したfirstNameとlastNameがレスポンスに表示されました。
- レスポンスヘッダーにも設定した値が反映されていました。

- Lambdaのログにも反映されていました。（分かりづらいですが。。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/1b5bfbc3-aacc-b85d-510c-d7df4230a9fb.png)

### 4.データテーブルを作成
:::note info
今回は、CORSの確認をするだけなので、DynamoDBの作成は省略しています。
:::

### 5.ウェブアプリケーションに対話性を追加
- Amplifyでデプロイしたhtmlから、API Gatewayをつなぎます。
#### コードの修正
- 以下のコードをindex.htmlに貼り付け、41行目のYOUR-API-INVOKE-URLをAPI Gatewayのオリジンに置き換えます。また、同様にZip圧縮します。
```
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
    <!-- Add some CSS to change client UI -->
    <style>
    body {
        background-color: #232F3E;
        }
    label, button {
        color: #FF9900;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 20px;
        margin-left: 40px;
        }
     input {
        color: #232F3E;
        font-family: Arial, Helvetica, sans-serif;
        font-size: 20px;
        margin-left: 20px;
        }
    </style>
    <script>
        // define the callAPI function that takes a first name and last name as parameters
        var callAPI = (firstName,lastName)=>{
            // instantiate a headers object
            var myHeaders = new Headers();
            // add content type header to object
            myHeaders.append("Content-Type", "application/json");
            // using built in JSON utility package turn object to string and store in a variable
            var raw = JSON.stringify({"firstName":firstName,"lastName":lastName});
            // create a JSON object with parameters for API call and store in a variable
            var requestOptions = {
                method: 'POST',
                headers: myHeaders,
                body: raw,
                redirect: 'follow'
            };
            // make API call with parameters and use promises to get response
            fetch("YOUR-API-INVOKE-URL", requestOptions)
            .then(response => response.text())
            .then(result => alert(JSON.parse(result).body))
            .catch(error => console.log('error', error));
        }
    </script>
</head>
<body>
    <form>
        <label>First Name :</label>
        <input type="text" id="fName">
        <label>Last Name :</label>
        <input type="text" id="lName">
        <!-- set button onClick method to call function we defined passing input values as parameters -->
        <button type="button" onclick="callAPI(document.getElementById('fName').value,document.getElementById('lName').value)">Call API</button>
    </form>
</body>
</html>
```

- Amplifyのマネコンで、先ほど作成したウェブアプリケーションを選択して、Zipファイルを置き換えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/b9a84c99-0764-caa0-3b5e-f86d52e75e88.png)

#### ウェブアプリケーションをテスト
- リクエストし、画面を表示する。FirstNameとLastNameに適当に文字列を入れて、Call APIをクリックする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/e0048d72-8e2f-451d-8fd0-2570b15b4dca.png)
- Hello from Lambda ${入力値}が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c4252f63-a031-cded-0986-e55b4be8fe3e.png)

## CORSを試す
### 拒否されるパターン
- CROSのAccess-Control-Allow-Originに、適当な値を設定しデプロイします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0fa66ac7-844f-ed7c-a679-2147a2240ca6.png)

- HTMLからCall APIをクリックすると、何も表示されません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/eadef7a0-2571-efed-ee66-01ec66f4b45e.png)

- ブラウザのコンソールを確認すると、確かに、適当に設定したAccess-Control-Allow-Origin「abcdefghijklmnopqrstuvwx」により、拒否されています。
```
Access to fetch at '${API Gatewayのオリジン}' from origin '{Amplifyのオリジン}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains the invalid value 'abcdefghijklmnopqrstuvwx'. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```
- このメッセージを見ると、リクエスト時に、「no-cors」を設定しても良いと書いてある。

### Access-Control-Allow-OriginにAmplifyのオリジンを指定するパターン
- 設定のエビデンスは省略するが、レスポンスは正常でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/c0aadf43-7aae-c1b5-a25e-124b859bff5a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/95fdad81-33a8-5024-e5b5-25d52a21c3e1.png)

### modeにno-corsを指定
- 再び拒否されるように、Access-Control-Allow-Originに適当な値を入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2ef3f898-7a1b-9688-8de3-a412271e2afd.png)

- この時点で、リクエストします。先ほどと同様に拒否されます。
```
Access to fetch at '${API Gatewayのオリジン}' from origin '{Amplifyのオリジン}' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains the invalid value 'abcdefghijklmnopqrstuvwx'. Have the server send the header with a valid value, or, if an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
```
- JavaScriptのrequestOptionで、modeをno-corsに変更し、Amplifyにデプロイします。
```
            var requestOptions = {
                method: 'POST',
                headers: myHeaders,
                body: raw,
                redirect: 'follow',
                mode:'no-cors'
            };
```
- コンソールログにエラーはCORSのポリシーエラーはでていないが、SyntaxErrorが表示されました。（SyntaxErrorは、corsのときにも表示されていた。）
```
error SyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at (index):44:40
```

- 44行目のJSON.parseでエラーになっているようです。no-corsでは、クロスオリジンリソース共有ができない場合に、エラーとはならず空のレスポンスが返却されるとのことなので、JSON.parseができずにエラーになったと推測します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/d64a4dcc-71f8-2f0e-80a3-8913d81dcf0d.png)

# 考察
- 今まで聞いたことがあるくらいのCORSやSame-origin Policyの挙動が少しは理解できた気がします。

# 参考
https://aws.amazon.com/jp/getting-started/hands-on/build-web-app-s3-lambda-api-gateway-dynamodb/

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

https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/xss.html

https://www.trendmicro.com/ja_jp/security-intelligence/research-reports/threat-solution/csrf.html

https://developer.mozilla.org/ja/docs/Glossary/Origin

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

https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy

https://zenn.dev/qnighy/articles/6ff23c47018380

https://www.w3.org/wiki/CORS