CORS設定だけでは防げない、同一オリジンポリシーのセキュリティ弱点とその対策
はじめに
フロントエンドのセキュリティの基礎についてCORS周りを学ぶ機会があったのでメモ。
同一オリジンポリシーとCORS設定、そしてその弱点と対策について整理した。
(素人なので間違っている部分あればご指摘いただけますと幸いです。。)
同一オリジンポリシーとCORS
同一オリジンポリシーとは
例としてWebサーバーとAPIサーバーが分かれている場合を考える。
ブラウザに搭載された同一オリジンポリシーにより、フロントエンドアプリケーションから異なるオリジンへリクエストした際、レスポンスはフロントエンドアプリケーションに渡るまえに破棄される。
これにより、攻撃者が設置した罠サイトから利用者を守ることができる。
CORSとは
フロントエンドアプリケーションから異なるオリジンへのリクエストをする場合は、同一オリジンポリシーを緩和する必要がある。
API側にCORS設定を行うことで同一オリジンポリシーを緩和することができる。
WebサーバーとAPIサーバーでオリジンが異なる場合は、CORS設定により同一オリジンポリシーを緩和して対応する。
同一オリジンポリシーとCORS設定だけではAPIへのリクエストを防げない
同一オリジンポリシーが適用されたとしても、ブラウザからフロントエンドアプリケーションへのレスポンスが破棄されるだけで、APIへのリクエストそのものは防げない。
そのため、攻撃者に対してAPIへの不正なアクセスを許してしまう。
プリフライトリクエストを強制させることによる対策
同一オリジンポリシーだけでは不十分なので、プリフライトリクエスト(preflight request)という機能がブラウザに搭載してある。
本来のリクエストに先立って、ブラウザが「このリクエストは安全か」をAPIサーバーに確認する。
APIが許可するリクエストでない場合、リクエストは送られずに破棄される。
フロントエンドアプリケーションからのリクエストが単純リクエストではない場合に、ブラウザはプリフライトリクエストを発生させる。
つまり、下記のような対策により単純リクエストを避けることでプリフライトリクエストを発生させることができる。
- fetch関数を使う
- 任意のカスタムヘッダを追加する
- Content-Typeを
application/json
にする
全てのリクエストに対してプリフライトリクエストを強制することができれば、APIへの不正なアクセスを防止できる。
例えば、APIの実装の中でリクエストのカスタムヘッダの有無を検証し、カスタムヘッダのないリクエストを拒否すれば、プリフライトリクエストを強制することができる。
下図はフローと設定のイメージ。
プリフライトリクエストの確認方法
Chromeのdev toolからプリフライトリクエストを確認するためには特別な手順が必要となることに注意。
ちなみに、プリフライトリクエストはブラウザの機能なので、例えばPostmanをつかってもプリフライトリクエストが発生しない。この場合はOPTIONS
メソッドでプリフライトリクエストを自作するしかない。
(Cookieもプリフライトリクエストもブラウザの機能なのに、Cookieは設定できてプリフライトリクエストは設定できないのはなぜなんですかね?)
同期通信によるリクエストは同一オリジンポリシーの対象外なので不正なアクセスを防げない
form要素などの同期通信によるリクエストは、そもそも同一オリジンポリシーが適用されないので不正なアクセスを防御できない。Cookieに保存されたログイン情報などを不正利用されてしまう上、フロントエンドアプリケーションに対してエラーも起きない。
同期通信によるリクエストを禁止する
上述の通り、API側でリクエストにカスタムヘッダを強制することで、プリフライトリクエストを発生させる。
form要素などの同期通信ではカスタムヘッダを設定できないので、同期通信を禁止してform要素からの不正なアクセスを防ぐことができる。
トークンを使って同期通信の認証を行う
正規のフロントエンドからform要素などの同期通信を使いたい場合、トークンを使った認証を行うことで罠サイトからの不正なアクセスを防げる。
ここでは、APIにセッションごとに変更されるトークンを返すエンドポイントを追加し、そのエンドポイントでは下記の2点を行う。
- トークンを返却する(*)
- Cookieにトークンを保存する
(*)
一般的にはAPIでのCookieの設定をHttpOnly
とするため、フロントエンドアプリケーションでCookieの中身を取得することができない。そのため、トークンそのものをレスポンスのBodyで返却する必要がある。
Cookieの設定をHttpOnly
としない場合は、フロントエンドアプリケーションのJavaScriptからCookieを読み込むことができる。そのためトークンそのものを返却する必要はない。この場合、ドメインが異なるサイトによるCookieへのアクセスはブラウザによって禁止されているので、罠サイトがCookieを読み込むことはできない。
フロントエンドアプリケーションは、まずトークンのエンドポイントに対してリクエストし、その後返却されたトークンをBodyに入れたformの送信を行う。
API側ではformのBodyに含まれるトークンと、Cookieに保存されたトークンを検証し、両者が等しい場合にリクエストを処理する。
罠サイトからのリクエストでは、同一オリジンポリシーによりトークンを取得することができないため、不正なアクセスは失敗する。
このような認証を行うために、いくつかのフレームワークではCSRFミドルウェアが提供されている。(echoの例)
参考
Discussion