これはなに?
セッションをクライアントサイドだけで管理するのは難しいのでやめようね。という話をする機会が稀にあるのですがどういうときに困るんだっけ?またはどういう値はクライアントサイドに持っていいんだっけ?というのを毎回自分で考えるのが面倒なので書きました。
ここではクライアントサイドだけでセッション管理するのが難しい理由について説明します。そのあとに翻って一般的にセッションはどういう性質を持っているのか(あるいは持っているべきか)という話をします。
クライアントサイドだけでやるセッション管理の例
Railsにはセッションストレージとして、CookieStoreというものがあります。説明に関しては
Railsセキュリティガイド: 2.3 セッションストレージ から引用しますが
RailsのCookieStoreはクライアント側のcookieにセッションハッシュを保存します。サーバーはこのセッションハッシュをcookieから取得することで、セッションIDの必要性を解消します。こうすることで、アプリケーションのスピードは著しく向上しますが、このストレージオプションについては議論の余地があるため、セキュリティ上の意味やストレージでの制約について以下の点を十分考えておかなければなりません
例えば、ログインが必要な機能にアクセスするごとに「このユーザはログインしてるんだっけ?」というチェックのためにサーバ側のストレージ(DB/KVSなど)にアクセスが走ると、同時接続数が多いアプリケーションだとそれだけでDBのread負荷が上がる可能性があります。このような時に例えばCookieStoreを使うとリクエストに乗ってくるcookieを見るだけで済むのでサーバサイドの負荷を軽減することができる、という利点があります。
あとは、jwtの中にセッション情報を全部ぶち込んでいるケースとかも同様だと思ってもらって良さそうです。
Cookie Storeを使うと難しいポイント
CookieStoreを使うとパフォーマンス向上してよかったね〜〜って話で終わるかというとそうではなくて、クライアントサイドでcookieを管理しているため前提としてcookieの有効状態を制御することは(少なくともサーバサイドにストレージがある時にくらべて)難しいです。
上で引用した箇所の下にも
- セッションcookieはひとりでに失効することはないため、悪用目的で使い回される可能性もあります。保存済みのタイムスタンプを利用して古いセッションcookieをアプリケーションで失効させるのもよい方法かもしれません。
という文章があります。
Cookie Storeに関して想定される攻撃例
具体的にどういう攻撃が考えられるか、またその対策については railsguides.jp のRailsセキュリティガイドの中から1つ、別のサイトから1つ紹介します。
再生攻撃
Railsセキュリティガイド: 2.5 CookieStoreセッションに対する再生攻撃 から引用します
再生攻撃のしくみは次のとおりです。
- ユーザーがクレジットを受け取る。総額はセッションに保存されているとする (これはあくまで説明のためのものであり、やってはいけません)。
- ユーザーがクレジットで何かを購入する。
- つかった分減ったクレジットがセッションに保存される。
- ここでユーザーの暗黒面が発動する。最初にブラウザに保存されていたcookieをコピーしてあったものを、現在のブラウザのcookieと差し替える。
- ユーザーのクレジット額が元に戻る。
とあり、これについての対策は
結論から言うと、 この種のデータはセッションではなくデータベースに保存するのが最善です。この場合であれば、クレジットをデータベースに保存し、logged_in_user_idをセッションに保存します。
上の説明にも (これはあくまで説明のためのものであり、やってはいけません)。
と書いてあるように、この例では自明ですが色々なタイミングで「ユーザは任意のタイミングまでセッションを巻き戻せる状態だけどこれセッションに持たせていいんだっけ?」みたいなことを考えていく必要があります。
セッションハイジャックされた時に追い出せないよ問題
また、セッション管理がクライアントサイドで完結しているとログアウト/パスワード変更などに対してセッションを無効化する処理ができないという問題があります。
Rails SessionにCookieStore使った時の問題点 から引用しますが(これの元ネタの記事はどうも消されてしまっているようです)
server-sideではstate管理しないので、当然remoteでセッションの無効化はできません。 つまりログアウトしてもsession cookieのtoken自体は無効化されません。
という話があり、特に困りそうなポイントとしてはその後に書いてある、
問題は、session cookieが無効化できないのに、それが永遠に有効であること。 FBとかY!Jとかでやってる「パスワード変更したら既存のsessionが無効になる」ってのはRailsは一切面倒見てくれないので、パスワード変えても漏洩したsession cookieは有効なままです。 なので、ひとたびsession cookieが漏れたら、完全にアウト。 なにやってもアカウント乗っ取られたまんま。永遠に。
こういうケースです。
で、これはどう対策すればいいの?という話ですが、この記事の中ではセッションにnonceをつければいいじゃないというアイデアが書いています。(これ自体はジャストアイデア的に書かれていて、その後にもっとシンプルで効果的な対策が書かれています)
OpenID Connectの名前にも触れていることからDBにnonceを保存して、セッション中にnonceがあったらDBと照合する、というような実装を想像しました。実はRailsセキュリティガイドの再生攻撃のあたりにもnonceを使えば防げるっちゃ防げるみたいな話は書いてます(この話は後ほど触れます)
そうすると、攻撃者が古いcookieを持ってきても「そのnonceはもうさっき見たので無理で〜す」みたいな感じで弾けるんですが、ここで みたいな顔になるポイントがあります。
人類はどうしてcookie storeを使っていたのか
という話に立ち戻るとRailsセキュリティガイドの中に
サーバーはこのセッションハッシュをcookieから取得することで、セッションIDの必要性を解消します。こうすることで、アプリケーションのスピードは著しく向上しますが
ということでクライアントサイドにセッションを完結させるのはレスポンス速度向上のためなんですね(ここが違うとこの後の話もおかしなことになりますが...)
再生攻撃のあたりでnonceって単語が出てるところを見ると
この再生攻撃は、セッションにnonce (1回限りのランダムな値) を含めておくことで防ぐことができます。nonceが有効なのは1回限りであり、サーバーはnonceが有効かどうかを常に追跡し続ける必要があります。複数のアプリケーションサーバーで構成された合いの子アプリケーションの場合、状況はさらに複雑になります。nonceをデータベースに保存してしまうと、せっかくデータベースへのアクセスを避けるために設置したCookieStoreを使う意味がなくなってしまいます。
こういう話があり、個人的にはCookieStoreの利点が薄れる + 実装の複雑度が増すので、nonceを使うというジャッジをするケースは少ないかなと思います
では、パスワードリセットなどの後にセッションを無効化するには?
CookieStore単体では(そして、クライアントサイドに完結したセッションストレージでは)セッションハイジャック対策として行われるセッション無効化ができないということがわかりました。
では、どうすればいいのかというとクライアントサイドでセッションを管理することを諦めてしまうのが一番簡単ではないかと思います。
引用元のサイトでも、このように触れられています(パフォーマンスに関しては、どういうケースでどういうボトルネックがあって、セッションの一部だけをクライアントサイドに逃がすとどういう風に解消しそうか、という話はまた別途議論が待たれる)
もしくはCookieStoreの代わりにMemcacheStore使うようにしてもいいです。 sessionをserver-sideで管理するようにさえすれば、この問題はそもそも発生しないですし。 パフォーマンスに影響しますけど。
セッションってどうあるべき?
ここまで、セッションをクライアントサイドだけで管理すると落とし穴あるよ!そもそもセッションの無効化ができないのである程度複雑な機能が入ってくるとクライアントサイドだけでの管理は無理だよ!って話をしましたが、じゃあお前はセッションをどうしたいの?というと
巻き戻ってもいい値を除いてユーザの状態はサーバサイドで管理すべき
だと思います。
ここでいう巻き戻ってもいい(または巻き戻っても特に変化しない)値の例としては
- ユーザID/名前
- ログイン日時
などが考えられます。このあたりをクライアントサイドに持たせてうまくストレージへのアクセス方法を減らすことはできるかもしれません
結論
ということでサーバサイドにセッションのストレージ(DBなりKVSなり)は用意したほうが良いと思います
参考
jwtでセッションを管理するのはやめようね、という話: Stop using JWT for sessions, part 2: Why your solution doesn't work - joepie91's Ramblings