Hasegawa方式の CSRF対策を試してみた
Amon2 での実装例です。
使用感としては、
- 実装はそれほどむずかしくない
- トークンの保存をサーバー側でやらなくていいので楽
といったかんじ。
管理画面とかでつかってみたらよいかもしれぬ。
use strict; use warnings; use utf8; use File::Spec; use File::Basename; use lib File::Spec->catdir(dirname(__FILE__), 'extlib', 'lib', 'perl5'); use lib File::Spec->catdir(dirname(__FILE__), 'lib'); use Amon2::Lite; { package Amon2::Plugin::Web::Hsegawa; use constant { REDIRECT => 10001, VALIDATION_ERROR => 10002, }; sub init { my ($class, $c, $conf) = @_; $c->add_trigger( BEFORE_DISPATCH => sub { my $c = shift; my $method = $c->req->method; if ($method ne 'GET' && $method ne 'HEAD') { unless (_validate_hsegawa($c)) { my $res = $c->render_json( {error => 'CSRF Detected.'}, ); return $res; } } }, ); Amon2::Util::add_method($c, 'post_redirect', \&post_redirect); Amon2::Util::add_method($c, 'post_validation_failed', \&post_validation_failed); } sub post_redirect { my $self = shift; my $res = $self->redirect(@_); return $self->render_json( { code => REDIRECT, location => scalar($res->header('Location')), } ); } sub post_validation_failed { my $self = shift; return $self->render_json( { code => VALIDATION_ERROR, message => $_[0], } ); } sub _validate_hsegawa { my $c = shift; return 0 unless defined $c->req->header('X-From'); return 1 unless defined($c->req->header('Origin')); return 1 if _is_same_origin(scalar($c->req->header('Origin')), scalar($c->req->header('X-From'))); return 0; } sub _is_same_origin { my ($a, $b) = @_; $a = URI->new($a); $b = URI->new($b); return ( $a->scheme eq $b->scheme && $a->host eq $b->host && $a->port eq $b->port ); } } our $VERSION = '0.01'; my @ENTRIES; sub load_config { +{} } get '/' => sub { my $c = shift; return $c->render('index.tt', { entries => \@ENTRIES, }); }; post '/inquiry' => sub { my $c = shift; my $msg = $c->req->param('msg'); if ($msg =~ /fuck/) { return $c->post_validation_failed('Bad word'); } warn "POSTTTTTTTTTTTTTTTTTTT: $msg"; unshift @ENTRIES, $msg; return $c->post_redirect('/'); }; # load plugins __PACKAGE__->load_plugin('Web::JSON'); Amon2::Plugin::Web::Hsegawa->init(__PACKAGE__); __PACKAGE__->enable_session(); __PACKAGE__->to_app(handle_static => 1); __DATA__ @@ index.tt <!doctype html> <html> <head> <meta charset="utf-8"> <title>hsegawa::CSRFTest</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css"> <link rel="stylesheet" href="[% uri_for('/static/css/main.css') %]"> </head> <body> <div class="container"> <header><h1>hsegawa::CSRFTest</h1></header> <section class="row"> <form method="post" action="/inquiry" class="csrf"> <input type="text" name="msg"> <input type="submit" value="Send" class="btn"> </form> </section> <div> [% FOR e IN entries %] [% e %]<br /> [% END %] </div> <footer>Powered by <a href="http://amon.64p.org/">Amon2::Lite</a></footer> </div> <script> "use strict"; $.fn.hsegawa = function (opts) { var form = $(this); form.submit(function () { $.ajax({ url: form.attr('action'), type: form.attr('method'), data: form.serialize(), headers: { 'X-From': location.href }, dataType: 'json' }).success(function (res) { if (res.code == 10001) { opts.redirect(res); } else if (res.code == 10002) { opts.validation_failed(res); } }).error(function (res) { opts.error(res); }); return false; }); }; $('form.csrf').hsegawa({ redirect: function (res) { location.href = res.location; }, validation_failed: function (res) { alert(res.message); }, error: function (res) { alert("ERROR: " + x); } }); </script> </body> </html> @@ /static/css/main.css footer { text-align: right; }