この記事は Play framework 2.x Java and 1.x Advent Calendar 2013 の21日目の記事です。
昨日は @yuba さんの ステートレスなPlay2でログイン状態を管理する でした。
明日はまだ未定のようです。
ちなみに野暮なつっこみを入れてしまいますが、@yuba さんの ステートレスなPlay2でログイン状態を管理する で
公式ドキュメントのコード例をみると、なんとユーザーIDをそのままクッキーに突っ込んでおられます。そりゃもちろんそれで動くけど、ユーザーIDを偽装し放題だよお… 脆弱性なんてチャチなもんじゃあ断じてねえ、もっと恐ろしいものの片鱗を…
という記述がありましたが、これは正しくありません。
というのも、Play は Session という仕組みが無い訳ではなく、きちんと提供しています。ただその実装が、Servlet の様にサーバ上で保持する実装ではなく、Railsなどと同様に Cookie に値を保存することで実現しているのです。
Playをはじめ Rails など Cookie を Session Store に利用するフレームワークでは、上記で懸念されている偽装(つまり値の書き換え)を防ぐために Cookie に署名をつけています。
したがって、ユーザが勝手に別のユーザIDに書き換えてリクエストを送ってきたとしても、署名が一致しないため値は無効とみなされます。なので上記で心配されているような脆弱性はそもそも存在しません。
Session に直接ユーザIDを格納する実装方法が問題となるのは、この Cookie が流出してしまったりした時に、該当のユーザID自体を破棄しない限り、Sessionを無効にする事ができない点にあります。
ユーザIDを破棄というのはなかなか現実的に難しいので、この問題を避けるために @yuba さんが提案するようなトークンを使う実装が望ましい訳です。
閑話休題。
Session Fixation と呼ばれる攻撃があります。どんな攻撃かは徳丸さんの解説などをご参照ください。
公式ドキュメント にある通り、Play の Session に認証情報を追加する実装していれば Session Fixation は行えません。
なぜなら、攻撃者が未ログイン状態の Cookie を強制して誰かにログインさせたとしても、攻撃者が保持している Cookie は未ログインのもののままなので、それを使って成りすます事はできないからです。
しかしながら、先ほど説明した通り公式ドキュメントの方法にも問題があるため、Cache API を使い、サーバ側で認証状態を管理する Stateful なアプローチを取ることにします。
サンプルコードはこんな感じ
public class Authentication extends Controller {
@Inject
static AuthenticationService authenticationService;
@Before(priority = 1)
public static void authenticate() {
final String userId = Cache.get("userId:" + session.getId(), String.class);
if (userId == null) Authentication.openLoginForm();
renderArgs.put("userId", userId);
}
public static void openLoginForm() {
render(); // ログインフォームを表示
}
public static void login(final String mailAddress, final String password) {
final String userId = authenticationService.login(mailAddress, password);
if (userId == null) {
params.keep();
flash.error("not match!");
Authentication.openLoginForm();
}
final String sessionId = Cache.get("token:" + userId, String.class);
Cache.delete("userId:" + sessionId); // ログインに成功したら該当ユーザの過去のセッションを破棄
Cache.safeSet("userId:" + session.getId(), userId);
Cache.safeSet("token:" + userId, session.getId());
Application.index();
}
...
}
Play1 の Session
には getId
というメソッドがあり、これがセッション毎にランダムなトークン文字列を作ります(実装的には UUID)。
もう少し細かく言うと、初めて getId
を呼び出した際にトークンを生成し、これを Cookie に格納します。2回目以降の呼び出しはこの格納された値を使います。
これで Cookie に格納されるのはランダムなトークンになり、流出してもそのトークンだけ破棄すれば良くなりました。この破棄は再ログインすることで実現できます。 ログインに成功したときに前のトークンを破棄しているためです。
これでばっちりですね。めでたしめでたし。
という訳にはいきません。
実は上記の実装には Session Fixsation ができてしまう脆弱性があります。
どういう事でしょうか? ポイントは session.getId()
にあります。順に攻撃シナリオを追っていきましょう。
- 攻撃者がこのシステムにアクセスしてくる。
- システムはまず authenticate() を呼び出して認証されているかチェックする。
- この時にシステムは
session.getId()
を呼び出している。 - なのでトークンが生成されて、それが Cookie として出力される。例としてここでは
AAAAAA
というトークンが発行されたとしよう。
- この時にシステムは
- もちろん認証情報は無いのでログインフォームにリダイレクトされるが、ここで攻撃者は
AAAAAA
というトークンが含まれた署名つき Cookie を手に入れることができる。 - 攻撃者はCookieMonsterやその他の方法を使って手に入れたCookieを攻撃対象ユーザに使わせる。
- 攻撃対象ユーザは Fixation された
AAAAAA
というトークン入り Cookie を使ってシステムにアクセスする。 - 攻撃対象ユーザがログインに成功すると、
AAAAAA
というトークンで Cache にユーザIDが保存される。 - その状態で攻撃者が
AAAAAA
というトークン入り Cookie を使ってシステムにアクセスすると、今度は Cache にユーザIDが保存されているため、攻撃対象者としてシステムにアクセスすることができる!!
という感じです。
認証の State を Cache つまりサーバ側に持ってしまった事によって、Cookie 自体の値が変わらなくても認証済みになってしまうという事ですね。
ではどうすればいいのか。
基本的には、ログインに成功したらトークンを新しく作り直す、という事です。
なので session.invalidateId()
のようなメソッドが存在していれば良いのですが、残念ながら Session
にはそのようなメソッドがありません。
従って方針としては2つあります。
session.getId()
を使わずに自前でトークンを発行し、ログイン成功時には新しく作り直す。- むりやり
session.getId()
が返す値を新しくする。
後者の例が以下になります。
public class Authentication extends Controller {
...
public static void login(final String mailAddress, final String password) {
final String userId = authenticationService.login(mailAddress, password);
if (userId == null) {
params.keep();
flash.error("not match!");
Authentication.openLoginForm();
}
final String sessionId = Cache.get("token:" + userId, String.class);
Cache.delete("userId:" + sessionId); // ログインに成功したら該当ユーザの過去のセッションを破棄
session.remove("___ID");
Cache.safeSet("userId:" + session.getId(), userId);
Cache.safeSet("token:" + userId, session.getId());
Application.index();
}
...
}
文字列で remove しているのが残念ですね。
本当は session.remove(Session.ID_KEY); とかできればいいのですが残念ながらパッケージprivate
という訳で Play1 で Session Fixation ができてしまう脆弱性を仕込んでしまった話でした。おしまい。
明日まだ未定っぽいので誰かやりましょう。