PlayFrameworkのCSRF tokenの実装について調査してみた
はじめに
こんにちは、今年1番の楽しみだったブレードランナーが今週末に迫り待ちきれない佐々木です。
今回はPlayFramework(以下Playと略します)のクロスサイトリクエストフォージの対策の実装の詳細について調査してみました。
この記事はPlayFramework 2.4の内容を元に記述されています。最新バージョンでは設計や実装が変更されている可能性があります。
CSRFとその対策
CSRFとは
この記事では詳細に説明しませんが、WikipediaではCSRF(Cross-site Request Forgeries)とは以下のように説明されています。
CSRF脆弱性とは以下のような攻撃(CSRF攻撃)を可能にする脆弱性を指す:攻撃者はブラウザなどのユーザ・クライアントを騙し、意図しないリクエスト(たとえばHTTPリクエスト)をWebサーバに送信させる。Webアプリケーションがユーザ・クライアントからのリクエストを十分検証しないで受け取るよう設計されている場合、このリクエストを正規のものとして扱ってしまい、被害が発生する。CSRF攻撃はURL、画像の読み込み、XMLHttpRequestなどを利用して実行される。
代表的な対策
OWASPによると代表的な対策として下記の3つが紹介されています。 この中でSynchronizer (CSRF) TokensはRailsやSpringをはじめとした多くのWAFで採用されています。
- Synchronizer (CSRF) Tokens
- Double Submit Cookie
- Encrypted Token Pattern
Playframeworkでの実装
ではPlayではどのような実装になっているのか?ですが、結論から言うと上記の中ではSynchronizer Tokensパターンが当てはまります。 ただしPlayの場合セッションがCookieに保存されるため、仕組みがやや複雑になっています。
PlayFrameworkのセッションの実装
Playではセッション情報をCookieに含めてクライアント側で保持するようになっています。
詳細は実装を見ていただくとして、ざっと下記のようにセッション情報のエンコードとデコードをしています。
セッションのエンコード
- セッションに保存する各値(key-value形式でどちらもStringで保持している) のkey-valueをそれぞれURLエンコードして
key=val&key2=val2&...
の形式に変換する - 1.とシークレットからHmacSHA1によって署名を生成する
- 1,2より
<署名>-<メッセージ>
の形式の文字列を生成し、これをcookieの値とする
セッションのデコード
- 署名部分とメッセージ部分を分割して、メッセージ部分の署名を生成し、クライアントから送信された署名と比較する.
- 署名が一致すればメッセージ部分を
&
と=
で分割しURLデコードしてkey-valueの形式に変換する
timing attack対策
デコード時の署名の比較時に、通常の文字列比較を用いると処理に要する時間からアルゴリズムや暗号鍵を推測される(いわゆるtiming attack)ため、 比較時にはあらゆる入力に対して同じ処理時間になる方法を採用する必要があります。 これは下記のように実装されています。
// Do not change this unless you understand the security issues behind timing attacks. // This method intentionally runs in constant time if the two strings have the same length. // If it didn't, it would be vulnerable to a timing attack. def safeEquals(a: String, b: String) = { if (a.length != b.length) { false } else { var equal = 0 for (i <- Array.range(0, a.length)) { equal |= a(i) ^ b(i) } equal == 0 } }
CSRF Tokenの実装
上記でセッション情報を保存するcookie文字列の生成方法がわかりました。次にCSRF Tokenの生成方法について説明します。 より詳しくは実装を確認してください。
トークンの生成
- トークンとしてランダムな文字列を生成する(
SecureRandom#nextBytes(12)
) - nonceとして現在時刻のミリ秒を求める(nonceはリプレイ攻撃の対策として埋め込む使い捨てのランダムな値です)
<トークン>-<nonce>
の形式の文字列を生成しメッセージとする- セッションと同様にメッセージの署名を生成し、メッセージと署名を合わせてトークンとする
- 生成したトークンをセッション(Cookie)とWEBページ(Formなどに含める)に含めて応答する
トークンの検証
- セッションとリクエストボディ(formなどで送信された)中のトークンをそれぞれデコードする
- 受信した文字列をトークン(ランダムな文字列)、nonce、署名に分割し、署名の検証を行う(ここでも上記の比較方法を用いる)
- 2つのトークンの比較を行う(ここでも上記の比較方法を用いる)トークンが一致すれば正常なリクエストであるとわかる
最後に
PlayframeworkでのセッションとCSRF Tokenの実装について詳細を調査しました。 timing attackやreply attachへの対策などが織り込まれていて非常に参考になりました。