package Mojolicious::Plugin::DeCSRF; use Mojo::Base 'Mojolicious::Plugin'; our $VERSION = '0.94'; sub register { my ($self, $app, $conf) = @_; my $base = Mojolicious::Plugin::DeCSRF::Base->new; $base->{token_length} = $conf->{token_length} if $conf->{token_length}; $base->{token_name} = $conf->{token_name} if $conf->{token_name}; $base->{on_mismatch} = $conf->{on_mismatch} if $conf->{on_mismatch}; push @{$base->urls}, $conf->{urls} if $conf->{urls}; $app->helper(decsrf => sub { $base->c(shift) }); $app->hook(before_dispatch => sub { my $c = shift; return $c if $base->c($c)->check; if ($base->on_mismatch && ref($base->on_mismatch) eq 'CODE') { $base->on_mismatch->($c); } else { $c->render( text => "Forbidden!", status => 403, ); } } ); } package Mojolicious::Plugin::DeCSRF::Base; use Mojo::Base -base; my $_token_checked = 1; has c => undef; has token_length => 4; has token_name => 'token'; has on_mismatch => undef; has urls => sub { [] }; sub check { my $self = shift; my $c = $self->c; my $token = $c->session($self->token_name); $_token_checked = 1; if ($self->_match($c->req->url)) { return 0 unless ( $c->req->param($self->token_name) && $c->req->param($self->token_name) eq $token ); }; return 1; } sub url { my $self = shift; my $url = shift // ''; my $c = $self->c; if ($_token_checked) { $c->session($self->token_name => $self->_token); $_token_checked = 0; } if ($self->_match($url)) { return $c->url_for($url)->query([$self->token_name => $c->session($self->token_name)]); } return $c->url_for($url); } sub _match { my ($self, $url) = @_; if (@{$self->urls} && $url) { foreach (@{$self->urls}) { return 1 if $self->c->url_for($url)->path =~ m;^$_$;; } } return 0; } sub _token { my @chars = ( "A" .. "Z", "a" .. "z", 0 .. 9, qw(@ $ - _) ); return join("", @chars[ map { rand @chars } ( 1 .. shift->token_length ) ]); } 1; __END__ =head1 NAME Mojolicious::Plugin::DeCSRF - Defend from CSRF attacks centrally. =head1 SYNOPSIS # Mojolicious::Lite #!/usr/bin/env perl use Mojolicious::Lite; plugin 'DeCSRF' => { on_mismatch => sub { shift->render(template => '503', status => 503); }, token_length => 8, token_name => 'csrf', urls => qw~/protected~ }; get '/' => sub { my $self = shift; } => 'index'; get '/protected' => sub { my $self = shift; } => 'protected'; app->start(); __DATA__ @@ layouts/default.html.ep <%= content %> @@ protected.html.ep % layout 'default'; Home @@ index.html.ep % layout 'default'; Protected @@ 503.html.ep Service error! =head1 DESCRIPTION L is a L plugin that defend the framework from CSRF attacks centrally. With "good" strategy you have flexible control of the urls. "Good" strategy is wrap all of the urls with decsrf->url(URL) and control all urls that must be protected at one place with decsrf->urls(). =head1 OPTIONS Options can change at any time. =head2 C<< decsrf->on_mismatch >> Set custom mismatch handling callback. Default is $self->render( text => "Forbidden!", status => 403); decsrf->on_mismatch( sub { shift->render(template => '503', status => 503); } ); =head2 C<< decsrf->token_length >> Set custom token length. Default length is 4 symbols from 'A-Z', 'a-z', '0-9', '@', '$', '-', '_' ranges. decsrf->token_length(40); =head2 C<< decsrf->token_name >> Set custom token name in url and session parameters. Default name is 'token'. decsrf->token_name('csrf'); =head2 C<< decsrf->urls >> Set urls that must be protected. perlre can used. decsrf->urls([qw~/protected /.*?ected~]); push @{decsrf->urls}, qw~/protected /.*?ected~; =head1 METHODS L inherits all methods from L and implements the following new ones. =head2 C $plugin->register(); Register plugin in L application. =head2 C<< decsrf->url >> Add 'token' param to url that match with decsrf->urls. #/protected?token=XXXX decsrf->url('/protected'); #/protected?foo=bar&token=XXXX decsrf->url('/protected?foo=bar'); =head1 AUTHOR Ilya Tokarev =head1 COPYRIGHT AND LICENSE Copyright (C) 2013, Ilya Tokarev. This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0. =head1 SEE ALSO L, L, L. =cut