背景・目的
JavaScriptを利用した複数オリジンへのアクセスを調べる機会がありましたので、整理します。
ここでは、複数オリジンの定義や登場した背景やどのように設定するかをまとめます。
まとめ
Webアプリケーションの前提として、同一オリジンポリシーが基本としてあります。しかし、Ajaxなどで別オリジンへのアクセスを行う場合には、CORSヘッダーによる制御が必要になります。
CORSの仕組みでは、単純リクエストとプリフライトリクエストにより挙動が変わります。
プリフライトでは、事前にお伺いを立てて確認します。それ以降は単純リクエストと同じ挙動です。
概要
CORSとは?
- Cross-origin resource sharingの略。
- mdnのドキュメントによると、以下のように書かれていました。
オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。
- 異なるオリジンへのアクセス時にHTTPヘッダーによる制御が必要。
- 同じオリジンへのアクセス時には、不要。
そもそも、Originとは?
- mdnのドキュメントによると、以下のように書かれていました。
ウェブコンテンツのオリジンOriginは、ウェブコンテンツにアクセスするために使われる URL のスキーム (プロトコル)、 ホスト (ドメイン)、 ポート によって定義されます。スキーム、ホスト、ポートがすべて一致した場合のみ、二つのオブジェクトは同じオリジンであると言えます。
- URLスキーム、ドメイン、ポートの組み合わせのこと。これを同一オリジンポリシーという。
- これらが全て一致したときに、同一オリジンとして判別される。
- 以下に、例を記載します。
結果 | ドメイン | 理由 |
---|---|---|
同一オリジン |
http://example.com/app1/index.html http://example.com/app2/index.html
|
スキーム (http) およびホスト (example.com) が同じなので同一オリジン |
http://Example.com:80 http://example.com
|
サーバーは既定で80番ポートで HTTP コンテンツを配信するため、同一オリジン | |
異なるオリジン |
http://example.com/app1 https://example.com/app2
|
スキームが異なる |
http://example.com http://www.example.com http://myapp.example.com
|
ホストが異なる | |
http://example.com 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」には、以下のように書かれていました。
ユーザのアクセス時に表示内容が生成される「動的Webページ」の脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。
CSRFとは?
- cross-site request forgeriesの略。
クロスサイトリクエストフォージェリ(CSRF)とは、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことです。掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまいます。
クロスオリジンアクセスを行うには?
- HTTPヘッダーとHTTPレスポンスヘッダーで制御するが、アクセス制御シナリオの例によると、単純リクエストとプリフライトリクエストの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が一致する場合
Access-control-originが一致しない場合
プリフライトリクエストによる制御について
- 単純リクエストによるHTTPリクエストと判断されなかった場合、ブラウザは自動で通常リクエストの前に、OPTIONメソッドでお伺いを立てます。これをプリフライトリクエストといいます。
- お伺い後、許可された後は、単純リクエストと同様です。拒否後はそこで終了になります。
プリフライトリクエストが許可された場合
プリフライトリクエストが拒否された場合
実践
- 実際に手を動かして、CORSの挙動を理解してみようと思います。
- とにかくWebアプリケーションを用意する必要がありますので、AWSの「基本的なウェブアプリケーションを構築する」を参考に環境を構築したいと思います。
事前準備
1.Webアプリケーション
コードの準備
- 以下のコードを,index.htmlファイルとして保存し、zip圧縮します。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
Hello World
</body>
</html>
Amplifyトップページ
デプロイ
-
以下を入力し、保存してデプロイをクリックします。
確認
2.サーバレス関数を作成する
Lambdaトップページ
コード
- 以下のコードを貼り付けて、「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"
}
- 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の作成」をクリックします。
新しいリソースとメソッドを作成
-
以下を入力し、保存をクリックします。
-
「API Gateway に、Lambda 関数を呼び出す権限を与えようとしています:」と表示されるので、OKをクリックします。
-
メソッド変更の確認で「はい、既存の値を置き換えます」をクリックします。
APIのデプロイ
APIの検証
{
"firstName":"Grace",
"lastName":"Hopper"
}
-
リクエスト本文に設定したfirstNameとlastNameがレスポンスに表示されました。
-
レスポンスヘッダーにも設定した値が反映されていました。
4.データテーブルを作成
今回は、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>
ウェブアプリケーションをテスト
CORSを試す
拒否されるパターン
-
ブラウザのコンソールを確認すると、確かに、適当に設定した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のオリジンを指定するパターン
modeにno-corsを指定
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ができずにエラーになったと推測します。
考察
- 今まで聞いたことがあるくらいのCORSやSame-origin Policyの挙動が少しは理解できた気がします。
参考