どうも、すなです。
ありがたいことに業務ではフロントエンドもバックエンドもAWSも触らせてもらってて今後自分がどの方向にいくのか不明ですが、いずれにしてもセキュリティ関連の知識はエンジニアとして習得しておくべきだなと思い下記の本で勉強中。
まだまだ小規模だけど何人かのユーザーが使うアプリ開発してることもあって、最低限のセキュリティは確保しておきたいなーと思っている。
XSSとかCSRFとかよく聞くしなんとなくは分かってるけど、具体的にどういう設定だと危険でどういうふうにすれば対策できるのかとかを体系的に学びたかった。
なのでこの本を通してセキュリティ関連で学んだことを定期的にアウトプットしていく予定。
今回はCORS(Cross-Origin Resource Sharing)について
前提
そもそもセキュリティの話でよく言われる悪意のある攻撃というのは能動的攻撃と受動的攻撃に分類される。
能動的攻撃とは、攻撃者が直接webサーバーを攻撃するもの。代表例はSQLインジェクションなど。
それに対して受動的攻撃とは、攻撃者が直接webサーバーを攻撃するのではなく、利用者のブラウザを通じて攻撃を行うというもの。
うーむ、せこい。
受動的攻撃の中にもいくつか分類はあるが、ここでは省略。
受動的攻撃を防ぐためにはブラウザとアプリケーションそれぞれで対策を行う必要がある。
アプリケーション側でどれだけ対策を行っていても、ブラウザに問題がある場合は安全を確保できない。
ここではブラウザ側の対策として同一オリジンポリシーについて見ていく
同一オリジンポリシー
これはとあるサイトのJavaScriptなどのクライアントスクリプトから別のサイトへのアクセスを禁止するためのセキュリティ上の制限のこと。
たとえばiframeなどを使ってあるページに別のページを埋め込んだときのことを考えてみる。
そのページのJavaScriptからiframeで埋め込んだページでJavaScriptアクセスができるとログイン情報などの秘匿情報が取得できてしまう可能性がある。

埋め込んだのが自分のサイトなら良いが、外部のサイトで勝手に埋め込まれてJavaScriptで情報を抜き取られたらたまったもんじゃない。
そのため、異なるホスト感でJavaScriptアクセスができないように、ブラウザでは同一オリジンポリシーというものが設定されている。
(*厳密にはリクエストの種類にもよるかもだけど詳細は分からず。。)
ではもう少し具体的に「同一オリジンである」とはどういう状況かというと、以下の項目を全て満たす場合である。
① URLのホスト(FQDN)が一致している
② プロトコルが一致している
③ ポート番号が一致している
気にかけるべきは特に1番目で、ここでいうとtrap.comとhost.comが異なっているため同一オリジンポリシーに引っかかる仕組みになっている。
サイト間でJavaScriptを使いたい場合はどうするの?
前述したように同一オリジンポリシーの制限によって、ドメイン(ホスト)が違うサイトではJavaScriptはアクセスできない。
しかし、悪意のあるサイトでは使われたくないが、信頼したこのサイトでは使いたいというようなわがままなニーズに答えるための仕様が存在する。
それがCORS(Cross-Origin Resource Sharing)である。
以下ではJavaScriptを使ったHTTPリクエスト(XMLHttpRequest)を送る場合を考える。
シンプルなリクエストの場合
シンプルなリクエストとはどういうものかという定義はいったん後回しにして、ここではとりあえずセキュリティ上の懸念がある程度ないものとだけ説明しておく。
そういったシンプルなリクエストではサーバー側の簡単な設定だけで異なるオリジンにHTTPリクエストを送ることが可能となる。
まずHTTPリクエストを送信した際、そのリクエストのHTTPヘッダにはOriginという項目があり、そこにそのサイトのドメイン情報が付与されている。
こんな感じ。
// HTTPリクエストヘッダ
GET https://...
...
Origin: http://example.com
...
サーバー側はリクエストに対するレスポンスとしてAccess-Control-Allow-Originというものを設定でき、リクエストのヘッダにあるドメインがここに記載されたものに含まれている場合は、異なるオリジンにHTTPリクエストを送ることができるという仕組みである。
以下はAccess-Control-Allow-Originが “*” になっているので、全てのサイトからJavaScriptによるリクエストを送ることができることを意味する。
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: *
つまり特定のサイトからのリクエストのみを受け付けたい場合は、ここを “*” ではなく、そのサイトのドメインに設定すればよい。
シンプルなリクエストとは?
上記は「シンプルなリクエスト」の場合の話である。
ではシンプルなリクエストとは何かというと、以下の条件をすべて満たすものである。
メソッドは下記のいずれか
・GET
・POST
・HEAD
リクエストヘッダは以下のものに限る
・Accept
・Accept-Language
・Content-Language
・Content-Type
Content-Typeヘッダは以下のいずれか
・application/x-www-form-urlencoded
・multipart/form-data
・text/plain
ぶっちゃけ全部はよくわかってない。。。
ただPOSTでよく使われるContent-Typeがapplication/jsonの場合は上記のケースに該当しない。
シンプルなリクエストじゃない場合
上記のシンプルなリクエストじゃない場合にはクロスオリジンでHTTPリクエストできないのかというとそんなことはない。
ちょっとだけフローが複雑になるだけである。
具体的にはプリフライトという仕組みを使う。
プリフライトとは?
異なるオリジンにあるリソースにHTTPリクエストを送る前に送るリクエストのことであり、これによって実際のリクエストを送信しても安全かどうかを確かめる。
今回はHTTPリクエストでContent-Typeにapplication/jsonを指定した場合を考える。
この場合、ブラウザは実際のリクエストを送信する前に以下のようなプリフライトのリクエストを送信する。
OPTIONS
...
Access-Contorol-Request-Method: POST
Access-COntrol-Request-Headers: content-type
Origin: http://example.com
...
プリフライトのリクエストはOPTIONSである。
そして実際のリクエストで送られる情報を通知している。
これによってサーバーはこのリクエストを受け入れるかどうかを決められる。
これを受け入れたサーバーは例として以下のようなレスポンスを返す
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Method: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 17280000
...
ブラウザはこのレスポンスを受けて初めて、実際のリクエストを送ることができるようになった。
このようなフローによって、異なるドメインでもHTTPリクエストが送ることができる。
認証情報を含むリクエストの場合はどうするの?
デフォルトではクロスオリジンへのHTTPリクエストではHTTP認証やCookieなどの認証に用いられるリクエストヘッダは自動的には送信されない。
クロスオリジンでもこれらを送るためにはwithCredentialsというリクエストヘッダをtrueにセットする必要がある。
そしてレスポンスでAccess-Control-Allow-Credentials: trueというヘッダを返す必要がある。
この2つが満たされていれば認証に用いられるリクエストヘッダを異なるオリジンへのリクエストに使うことができる。
まとめ
今回はクロスオリジンへのアクセスがなぜ危険なのか、どうすれば安全にできるのかをまとめた。
今回の内容はAWSでAPI GatewayとLambdaを使ってAPIを構築していた時によく使ったものばかりだったので、この機会に体系的に学んでアウトプットできて非常に理解が定着したのでよかった。
セキュリティシリーズはまだまだ学ぶことたくさんありそうなので随時まとめていく予定。