Session Fixation

XSS や事前に取得したセッションを利用者に送り込んで権限を昇格させる類の攻撃で、セッションIDをクエリに含ませたり、シーケンシャルなセッションIDを用いるような愚かなことをしなければ基本的に問題はないものの、 Session Fixation への根本的な対策として、ログイン成功後にこれまでのセッションを破棄し、新しいセッションを発行することが望ましい。

Session Fixation について詳しいことは IPA の出している『安全なウェブサイトの作り方(pdf)』改定第4版 p.15 から p.20 に説明があります。

0.13

今まで P::M::Session ではログイン成功後に新しくセッションを開始するのにちょっとした細工が必要でしたが、 0.13 からは古いセッションの破棄とあたらしいセッションの発行を、

$request->env->{'psgix.session.options'}->{change_id}++;

することで容易に行えるようになりました

追記

$request->session_options->{change_id}++;

こちらの方がいいですね!

検証用

ログイン時にあたらしいセッションが発行されるかをチェックするために簡略化した検証用のコードを書いてみました。

/ -> /login -> / と遷移したときに、セッションが変わっていることがわかりますね!

use strict;
use warnings;

use Plack::Builder;
use Plack::Session 0.13;
use Plack::Session::Store::File;
use Plack::Session::State::Cookie;

my $app = sub {
    my $env = shift;
    my $request = Plack::Request->new($env);

    my $body = q{
        <html><head><title></title>
        <script>document.write(document.cookie);</script>
        </head><body></body></html>
    };

    if ($request->path_info eq '/') {
        my $session = Plack::Session->new($request->env);

        if ($session->get('verified')) {
            my $res = $request->new_response(200);
            $res->content_type('text/html');
            $res->body($body, ' verified session');
            $res->finalize;

        } else {
            my $res = $request->new_response(200);
            $res->content_type('text/html');
            $res->body($body, ' session is not verified');
            $res->finalize;
        }

    } elsif ($request->path_info eq '/login') {
        my $session = Plack::Session->new($request->env);

        if ($request->param("password") eq 'foo') {
            $request->session_options->{change_id}++;
                # is equals to $request->env->{'psgix.session.options'}->{change_id}++;
            $session->set('verified', 1);

            my $res = $request->new_response(200);
            $res->content_type('text/html');
            $res->body($body, ' correct password');
            $res->finalize;

        } else {
            my $res = $request->new_response(200);
            $res->content_type('text/html');
            $res->body($body, ' incorrect password');
            $res->finalize;
        }

    } elsif ($request->path_info eq '/logout') {
        my $session = Plack::Session->new($request->env);
        $session->expire;

        my $res = $request->new_response(200);
        $res->content_type('text/html');
        $res->body($body, ' logout');
        $res->finalize;

    } else {
        my $res = $request->new_response(404);
        $res->body("Not Found");
        $res->finalize;
    }

};

builder {
    enable 'Session',
        store => Plack::Session::Store::File->new(
            dir => './sessions'
        ),
        state => Plack::Session::State::Cookie->new(
            session_key => 'sid'
        );
    $app;
};