ja ja en nb ja en ja ja ja fr ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja en fa ja ja ja en ja es ja en ja ja ja ja ja ja ja ja ko ja ja ja en ja en ja ja ja en ja ja ja ja ja ja en ja ja en ja ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja en nl ja de ja ja de ja ja nb ja ja nb ja ja nb ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja jbo en en ja nl nb en ko it en it it it en fr it ja ja en la fi ja it it it pt it de en ja en en it fr it en it nb it ja ja ja ja en ja ja ja ja ja ja ja ja ja
API Key認証付きのAPI GatewayからS3へリダイレクトしたい(できない) - Sanwa Systems Tech Blog
AWS Lambda API Gateway CORS
表題の通りです。できない理由を書きます。また、ついでにSame-Origin PolicyとCORS (Cross-Origin Resource Sharing)についても簡単に解説します。
とあるデータ(刻々と追加されていきます。更新はなし)をDynamoDBに溜め込んでいます。そのデータを検索して返すWeb APIをAPI Gateway + Lambdaで実装しました。
このWebページにはJavaScriptが書いてあります。スクリプトはWebページを開くと同時に勝手に上記のURLにXHRでアクセスし、取得したデータを全て外部に送信してしまいます。
このようなことではいけません(私は死にたくありません)。もちろんこの秘密のデータを他のWebサイトから参照したいような状況もありますが、少なくとも全ての外部Webサイトのスクリプトからアクセスできる必要はまったくありません。そこで、スクリプトは原則として「同一のオリジン(場所)」にあるリソースにしかアクセスできないというルールができました。これが同一オリジンポリシーです。
オリジンという言葉が突然出てきましたが、これは大ざっぱに言えばドメインのことだと理解すればいいです。厳密にはRFC 6454で定義されていて、
HTTP/HTTPSの区分
FQDN(example.comとか、www.example.comとか)
をセットにしたものです*2。つまりhttp://evil.com上にあるスクリプトは、次のような場所にあるリソースにはアクセスできないわけです。
http://mycompany.local(ドメインが全然違う)
https://evil.com(同じに見えるけどHTTPとHTTPSが違う)
http://evil.com:8080(ポート番号が80番と8080番で違う)
http://www.evil.com(これも別扱い)
これだけ厳密なチェックが入るなら安心ですね。めでたしめでたし。
CORS (Cross-Origin Resource Sharing)
ところが、異なるオリジンをまたいで外部リソースを参照することが一切許されないのでは不都合なことも多々出てきます。そこで、リソースが置いてある側、つまりmycompany.local側で事前に設定を行い、どのようなタイプのリクエストなら許可するかをホワイトリスト形式で指定することになりました。これがCORS (Cross-Origin Resource Sharing)です。
具体的には、ブラウザからのリクエストに応じ、リソースを置いてあるサーバーがHTTPヘッダで「どこからのアクセスなら許可するか」「リクエスト時に含まれていても良いHTTPヘッダ」のような項目を回答します。ブラウザはこの値をチェックし、リクエストの内容がそれとマッチするときにだけアクセスを許可します(そうでないならアクセスをブロックしてネットワークエラーを発生させます)。この制御により、リソースの所有者の意図、ブラウザの利用者の意図に反してアクセスが行われてしまうことを防ぐわけです。
余談:CORSは何でないか
CROSはあくまでブラウザが(いわば)自主規制をするだけのもので、ユーザーが意図的に発生させたリクエストをブロックするようなものではありません。悪意を持って作成されたスクリプトを実行してしまっても、リソースの所有者やユーザーが意図しないうちに勝手にリクエストが発生してしまう(発生させられてしまう)ことを防ぐための仕組みです。セキュリティについてはサーバーサイドできちんと考える必要があります。
2種類のクロスオリジンリクエスト
このように、アクセス元のオリジン(上記の例ですとhttp://evil.com)、リソースのあるオリジン(同じくhttp://mycompany.local)とが異なる場合のアクセスはクロスオリジンリクエストと呼ばれます。ブラウザはこのリクエストを2種類に分けて扱います。
シンプルなクロスオリジンリクエスト
シンプルなクロスオリジンリクエスト(Simple cross-origin request)とは、特定のHTTPヘッダのみ・特定のContent-Typeの値のみを持つGET/POST/HEADリクエストです(詳しくはリンク先を読んでください)。jQueryでこのようなコードを書いた場合がこれに該当します*3。
$.ajax({ url: "https://...", type: "GET", headers: {} }).done(function(data) { console.log(data); });
スクリプトが「シンプルなクロスオリジンリクエスト」を送信した場合、ブラウザはまずレスポンスヘッダの中からAccess-Control-Allow-Originヘッダの値を確認します。その値が元のオリジンと一致すれば(*やhttps://*.example.comなどのワイルドカードも使用可)そのアクセスは認められることになります。逆に一致しないか、そもそもAccess-Control-Allow-Originヘッダがなかった場合、そのアクセスはブロックされてしまいます。
シンプルではないリクエスト
前項のシンプルなクロスオリジンリクエストではないリクエストは全てこれです。認証のためにAuthorizationヘッダやX-Api-Keyヘッダを付加する場合、PATCHリクエストやDELETEリクエストを送信する場合などが該当します。たとえばBASIC認証が必要なURLにアクセスするため、jQueryで次のようなコードを書いたとします。
$.ajax({ url: "https://...", type: "GET", headers: {"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="} }).done(function(data) { console.log(data); });
この場合、ブラウザはGETリクエストに先立ち、同じURLに対して自動的にOPTIONSリクエストを送信します。これはプリフライト(preflight)と呼ばれ、スクリプト側からは制御できず強制的に行われます。クロスオリジンリクエストを受け付けたいWebサーバーは、このOPTIONSリクエストに対して「どのようなリクエストなら受け付けても構わないか」を返答しなければなりません。返答は全てレスポンスヘッダで行います*4。
API Gateway
以下、API GatwayからLambda (Node.js)のコードを呼び出し、そのまま結果を返すか、またはS3へリダイレクトすることを考えます。また、API Gateway側は「Enable CORS」の設定を済ませていて、さらにLambda Proxy integrationを使うことを前提にします。リクエストはGETを利用します。
A. シンプルなクロスオリジンリクエスト&リダイレクトを行わない場合
一番単純なパターンです。ブラウザが前述の「シンプルなクロスオリジンリクエスト」を送信してきて、かつリダイレクトを行わない場合です。Lambda側の処理はこうなります。
callback(null, { statusCode: 200, body: JSON.stringify({message: "nya-!"}), headers: { "Access-Control-Allow-Origin": "http://example.com", "Content-Type": "application/json; charset=utf-8" } });
ブラウザはここでセットしたAccess-Control-Allow-Originヘッダの値を確認し、アクセス元のオリジンと一致するかどうかを調べて、問題がなければアクセスを許可します。API Gateway側のCORS設定は使用されません。
B. シンプルではないクロスオリジンリクエスト&リダイレクトを行わない場合
最初にブラウザがプリフライトリクエストを送ってきますが、これにはAPI Gatewayが勝手に応答します*5。レスポンスヘッダにはAccess-Control-Allow-Origin: *という内容が含まれていて*6、ブラウザは最初のチェックを行います。
このチェックをパスしたら、ブラウザは改めてGETリクエストを送りますが、後の処理は前項(A)と同じです。再度Access-Control-Allow-Originのチェックがかかるところも変わりません。
C. シンプルなクロスオリジンリクエスト&リダイレクトを行う場合
可能ですが制約が厳しくなります。
callback(null, { statusCode: 303, // See Other body: "", headers: { "Access-Control-Allow-Origin": "http://example.com", Location: `https://s3-ap-northeast-1.amazonaws.com/BUCKET-NAME/path/name.json` } });
最初のアクセスに対してAPI Gatwayは303を返します。ブラウザはAccess-Control-Allow-Originをチェックした上でリダイレクト、つまり指定されたS3へ再度GETを送ります。
ところがこの2回目のGETでは、ブラウザはオリジンとしてnullを使用します*7。この挙動はRFCで規定されています*8。したがって、リダイレクト先のS3側ではAccess-Control-Allow-Origin: *を返す必要があります。CORSでのアクセス制御がかけられないことを意味していて、重要なデータを扱うには非現実的になります。
cors - Are there any browsers that set the origin header to "null" for privacy-sensitive contexts? - Stack Overflow
D. シンプルではないクロスオリジンリクエスト&リダイレクトを行う場合
Chrome cancels CORS XHR upon HTTP 302 redirect - Stack Overflow
こちらで紹介されているW3C Recommendationにはこのような記述があります。
If the response has an HTTP status code of 301, 302, 303, 307, or 308 Apply the cache and network error steps.
つまり、このような処理が行われます。
最初にブラウザはプリフライトリクエストとしてOPTIONSリクエストを送信します。
API GatewayはHTTP 200 OKを返します。レスポンスにはAccess-Control-Allow-Originも含まれます。
次にブラウザはGETリクエストを送信します。
API GatewayはLambdaを実行し、上記のコードに従ってHTTP 303 See Otherを返します。
ブラウザは無条件で通信をキャンセルします。
API GatewayでAPI Key認証を行うためにはリクエスト時にX-Api-Keyヘッダを含める必要があります。このヘッダを含めた場合、リクエストは「シンプルなクロスオリジンリクエスト」ではなくなってしまいます。するとCではなく)のパターンになり、ブラウザは仕様上通信を行うことができません。
*1:同僚のSlackのアカウントを乗っ取って「これを見てください」と言ってURLを送るとか
*2:データURIなどについても記述がありますが省略します
*3:本当は jquery.get() で書けますが比較のために jquery.ajax() で書いています
*4:OPTIONSリクエストですからボディ部はありません
*5:「Enable CORS」の設定を行ったときに追加されたOPTIONSメソッドの設定に従ってレスポンスが送信されます
*6:"*"はデフォルト値です。もちろん変更も可能です
*7:Chrome 56.0.2924.87, Firefox 51.0.1で確認
*8:ただし「リダイレクトのときはこうなる」と明示されているわけではなく、ブラウザの実装に依存します
nurenezumi 2017-03-10 14:47
Amazon S3の機能まとめ
こんにちは、AWS担当のwakです。そろそろAWSソリューションアー…
S3 + AWS Cognito + Google認証でドメイン制限のついた非公開サイトを作る
こんにちは、AWS担当のwakです。 弊社では全社的にG Suiteを導…
AWS LambdaからGoogle APIを呼び出す
こんにちは、AWS担当のwakです。間が空いてしまったので、今回…
AWSの社内勉強会を開きました
こんにちは、AWS担当のwakです。 AWSを弊社で利用し始めて2年ほ…
AWS LambdaのパラメータをDynamoDBで管理する
こんにちは、AWS担当のwakです。前回に続きAWS Lambdaの話をし…
« Analytics の内容を Slack でグラフ表示 Slackにスレッド表示機能が追加されました »