Rails SessionにCookieStore使った時の問題点

今日 @mad_p さんからRT来てたこのツイートに関して、ちょっと調べたのでまとめときます。

前提条件

Railsではデフォルトでsessionをcookieにのみ保存して、DBなりmemcacheなりのserver-side storageには何も保存しません。 これがCookieStoreとか呼ばれてるやつです。

この場合のsession cookieは、Railsのsession object (Hash object) をMarshal.dumpしてそれに署名を付けたtokenです。 rails 4では署名付ける代わりに暗号化してるけど、ここで述べる問題に関してはsignedなのかencryptedなのかは関係ないのでその点は無視していいです。

「current user identifierを含むSigned JSONをcookieに入れてるのと同じなので、OpenID ConnectのID Tokenがcookieに入ってるようなもんだと思えばいい」と言えば、#idcon に来たり定期的にOAuth.jp読んだりしてる人には通じるだろうと期待します。

問題点

server-sideではstate管理しないので、当然remoteでセッションの無効化はできません。 つまりログアウトしてもsession cookieのtoken自体は無効化されません。

これを確認するには、サイトにログインしてCookie Headerから_foo_cookieってやつ見つけ出してその値をコピーして、CharlesでRequest Cookie Headerの_foo_sessionの値を常にそのコピーした値に書き換えてやるようなRewriteルールを設定してやって、サイトからログアウトしてみるとよいです。 通常server-sideでsession管理してるサービスだとこれでログアウトされるけど、CookieStoreを使ってる場合はこの条件の元ではログアウトしてもログイン状態のままになります。

でもまぁこれはCookieStoreの仕組み上どうしようもないのです。

問題は、session cookieが無効化できないのに、それが永遠に有効であること。 FBとかY!Jとかでやってる「パスワード変更したら既存のsessionが無効になる」ってのはRailsは一切面倒見てくれないので、パスワード変えても漏洩したsession cookieは有効なままです。 なので、ひとたびsession cookieが漏れたら、完全にアウト。 なにやってもアカウント乗っ取られたまんま。永遠に。

解決策

こちらのブログ記事は2011年のものなので、この問題自体は古くから知られている問題なのだと思います。僕知らなかったけど :p

この記事では以下のように「SSL使えばいいよ」って解決策示されてるけど、SSL化はsession cookieが漏洩する可能性を低くするだけで、「session cookie tokenが永遠に有効である」という状況を回避するものではないです。

The best and easiest solution is simply to use SSL. Not just on your login forms and actions, but your entire site, or at least any pages where you have sessions turned on. With SSL on, the user will not be able to replay your cookies and the entire attack vector is shut down. Rails 3.1 has a handy force_ssl switch you can use, and you can use something like:

「session cookie tokenが永遠に有効である」という状況を回避したい場合は、各デベロッパーが session[:expires_at] = 1.hour.from_now とかして session cookie token 自体に有効期限を含めて、それを毎リクエストごとにチェックする必要があります。

OpenID ConnectのID Tokenでも同じ問題が発生しうるので、ID Tokenは有効期限 (exp) をtoken自体に含んでいます。 で、ID Tokenを受け取ったサービスは、署名検証と同時に有効期限もチェックすることで、ID Tokenが漏洩してもなりすましリスクを一定期間内に狭めることができます。 ID Tokenはnonceを含むこともできる (し多分通常はnonce付いてる) ので、onetime tokenとして利用することもできます。 onetime tokenとして扱うと、なりすましリスクはかなり限定的になるでしょう。(ゼロではない)

…ってことで、RailsのsessionでもOpenID ConnectのID Tokenが含んでるような情報を含むようにして、ID Tokenのverificationと同じような処理をsession受け取った際にやってやれば、それで良いのです。 あとはConnectの仕様さえ読めばOKェ

もしくはCookieStoreの代わりにMemcacheStore使うようにしてもいいです。 sessionをserver-sideで管理するようにさえすれば、この問題はそもそも発生しないですし。 パフォーマンスに影響しますけど。

捕捉

Railsにはconfig/initializers/session_store.rbにはexpires_afterってオプションもあるけど、こちらはあくまでcookieの有効期限であって、悪意の無い通常のユーザーが普通にブラウザでアクセスしてればその期間すぎたらブラウザがcookieを削除してくれるけど、悪意あるユーザーはわざとcookieの有効期限無視してsession cookie送りつけてきたりするので、そこの設定はこのケースでは意味ない。

ログアウト処理でsession.delete (session.clearだっけ?ちょっと記憶曖昧) すると、RailsはSetCookieヘッダーを返してブラウザからそのsession cookieを削除しようとします。 が、そのsession cookieを削除するかどうかの決定権は、server-sideではなくclient-sideに存在します。

Comments