Edited at

Mojolicious のセキュリティ対策

More than 1 year has passed since last update.


Mojolicious の XSS 対策

Mojolicious の XSS 対策は簡単で、テンプレートで <%= => 形式で記述すればエスケープで出力されます。

<%== => で書くとエスケープされない。

<input name="company_name" type="text"value="<%= $company_name %>">

XSS は javascript が画面に表示されて実行されることによる問題なので

出力時にエスケープすれば問題ありません。

HTML メールを送っている場合も同様です。

メールテンプレートでエスケープすればいい。


Mojolicious のセッション漏洩、セッションハイジャック対策

ご存知の方も多いと思いますが Mojolicious はセッション機能に問題があります。普通はセッションIDのみを cookie に保存しデータはサーバ側に保存しますが、Mojolicious の場合はデータをパスフレーズと組み合わせて Base64 にエンコードして cookie に保存しています。Base64 はデコード可能なのでパスフレーズが分かれば値が見えてしまいます。

また、セキュリティとは関係ないですがデータを cookie に入れているのでデータが大きいと正常に動かなくなります。

以下はデフォルトのままセッションを保存したものですが、様々な問題があることがわかります。

対策前.jpg

設定しだいである程度は対策できますが、cookie にデータそのものを保存するという仕様はどうにもできません。

この対策としては他のライブラリを使うことです。僕の場合はオーソドックスに CGI::Session を使いました。

use CGI::Session;

use CGI::Session::ExpireSessions;
use Mojolicious::Lite;

my $app = app;
my $session;

post '/send' => sub {
my $c = shift;

# セッションを読み込む
&load_session($c);

# 何らかの処理
};

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ■ セッション
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
sub load_session
{
my $c = shift;

# セッションを保存するディレクトリ
my $session_dir = defined $c->app->config->{'session'}->{'session_dir'} ? $c->app->config->{'session'}->{'session_dir'} : 'session';
# 有効期限
my $expire = defined $c->app->config->{'session'}->{'expire'} ? $c->app->config->{'session'}->{'expire'} : 1200;
# クッキー名
my $cookie_name = defined $c->app->config->{'session'}->{'cookie_name'} ? $c->app->config->{'session'}->{'cookie_name'} : 'htwsid';
# パス
my $path = defined $c->app->config->{'session'}->{'path'} ? $c->app->config->{'session'}->{'path'} : '/';
# cookieはSSLのみ
my $secure = defined $c->app->config->{'session'}->{'secure'} ? $c->app->config->{'session'}->{'secure'} : 1;
# cookieはhttpのみ
my $httponly = defined $c->app->config->{'session'}->{'httponly'} ? $c->app->config->{'session'}->{'httponly'} : 1;

# cookie名をセット
CGI::Session->name($cookie_name);

# 古くなったセッションファイルを削除 1h
CGI::Session::ExpireSessions -> new(temp_dir => $session_dir, delta => 3600 ) -> expire_file_sessions();

# GETリクエストによるセッションIDの指定の無効化
$c->redirect_to('/') and exit if $c->param($cookie_name);

# cookieからセッションIDを探す。なければ undef。
my $sid = $c->signed_cookie($cookie_name) || undef;
$session = CGI::Session->load(undef, $sid, { Directory => $session_dir }) or die CGI::Session->errstr();

# 取得したセッションidが有効ならそのまま。無効(空or有効期限切れ)なら別のidを発番。
if ( $session->is_empty || $session->is_expired )
{
$session = $session->new(undef, $sid, { Directory => $session_dir }) or die $session->errstr;
}

# cookieをセット
$sid = $session->id();
$c->signed_cookie( $cookie_name, $sid, { expires => time + $expire, path => $path, secure => $secure, httponly => $httponly } );
}

対策後の cookie の状態。

対策後.jpg


  • セッションIDのキー(クッキー名)をオリジナルにすることで使われているフレームワークを予測できなくします。

  • 保存されるセッションIDは72文字で、ブルートフォース攻撃されてもそれなりに時間がかかるでしょう。

  • 有効期限を短くすればブルートフォース攻撃でセッションIDが分かった頃には期限が切れて使えなくなっています。

  • cookie を HTTP 通信のみにすることで、仮に XSS 脆弱性があっても javascript で cookie(セッション)を盗めなくなります。

  • cookie を SSL 通信のみにすることでセッションIDが常に暗号化されるので、セッションIDが推測されにくくなります。

  • GET クエリでセッションIDを送ってきたらリダイレクトしてセッションIDを指定できないようにする。


Mojolicious の CSRF 対策

最新の Mojolicious では CSRF 対策がされているようです。

4.59から導入されたCSRF対策を使ってみる

サーバ環境の問題で古い Mojolicious を使う必要があったので、以下のプラグインをカスタマイズしました。

Mojolicious::Plugin::CSRFDefenderというのを作った

カスタマイズといっても、エラーだった場合の処理を変えているだけですが...


CSRFDefender.pm

#    # input check

# $app->hook(after_static_dispatch => sub {
# my ($c) = @_;
# unless ($self->_validate_csrf($c)) {
# my $content;
# if ($self->error_template) {
# my $file = file($self->error_template);
# $content = $file->slurp;
# }
# else {
# $content = $self->{error_content},
# }
# $c->render(
# status => $self->{error_status},
# text => $content,
# );
# };
# });

# input check
# CSRFエラーの場合はフォームに戻る
$app->hook(after_static_dispatch => sub {
my ($c) = @_;
unless ($self->_validate_csrf($c)) {
$c->redirect_to('/');
};
});


Mojolicious のセッション機能を使うので、Mojolicious でできるセッション対策を施しておきます。

# cookieを暗号化

app->secret('passphrase');
app->sessions->secure(1);
app->sessions->cookie_name('csrftoken');