Skip to content

Commit

Permalink
added basic CSRF protection support
Browse files Browse the repository at this point in the history
  • Loading branch information
kraih committed Nov 30, 2013
1 parent c120d7d commit 18f4e12
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 4 deletions.
6 changes: 4 additions & 2 deletions lib/Mojolicious/Controller.pm
Expand Up @@ -416,9 +416,11 @@ sub url_for {
}

sub validation {
my $self = shift;
my $self = shift;
my $token = $self->session->{csrf_token};
return $self->stash->{'mojo.validation'}
||= $self->app->validator->validation->input($self->req->params->to_hash);
||= $self->app->validator->validation->input($self->req->params->to_hash)
->csrf_token($token);
}

sub write {
Expand Down
15 changes: 14 additions & 1 deletion lib/Mojolicious/Plugin/DefaultHelpers.pm
Expand Up @@ -2,7 +2,7 @@ package Mojolicious::Plugin::DefaultHelpers;
use Mojo::Base 'Mojolicious::Plugin';

use Mojo::ByteStream;
use Mojo::Util 'dumper';
use Mojo::Util qw(dumper sha1_sum steady_time);

sub register {
my ($self, $app) = @_;
Expand All @@ -28,6 +28,7 @@ sub register {
$app->helper(config => sub { shift->app->config(@_) });
$app->helper(content => \&_content);
$app->helper(content_for => \&_content_for);
$app->helper(csrf_token => \&_csrf);
$app->helper(current_route => \&_current_route);
$app->helper(dumper => sub { shift; dumper(@_) });
$app->helper(include => \&_include);
Expand Down Expand Up @@ -55,6 +56,12 @@ sub _content_for {
return $c->{$name} .= ref $content eq 'CODE' ? $content->() : $content;
}

sub _csrf {
my $self = shift;
$self->session->{csrf_token}
||= sha1_sum($self->app->secret . steady_time . rand 999);
}

sub _current_route {
return '' unless my $endpoint = shift->match->endpoint;
return $endpoint->name unless @_;
Expand Down Expand Up @@ -155,6 +162,12 @@ named buffers are shared with the L</"content"> helper.
% end
%= content_for 'message'
=head2 csrf_token
%= csrf_token
Get CSRF token from session, and if none exists generate one.
=head2 current_route
% if (current_route 'login') {
Expand Down
14 changes: 14 additions & 0 deletions lib/Mojolicious/Plugin/TagHelpers.pm
Expand Up @@ -15,6 +15,7 @@ sub register {

$app->helper(check_box =>
sub { _input(shift, shift, value => shift, @_, type => 'checkbox') });
$app->helper(csrf_field => \&_csrf_field);
$app->helper(file_field =>
sub { shift; _tag('input', name => shift, @_, type => 'file') });

Expand All @@ -41,6 +42,11 @@ sub register {
$app->helper(text_area => \&_text_area);
}

sub _csrf_field {
my $self = shift;
return $self->hidden_field(csrf_token => $self->csrf_token, @_);
}

sub _form_for {
my ($self, @url) = (shift, shift);
push @url, shift if ref $_[0] eq 'HASH';
Expand Down Expand Up @@ -304,6 +310,14 @@ picked up and shown as default.
<input name="background" type="color" value="#ffffff" />
<input id="foo" name="background" type="color" value="#ffffff" />
=head2 csrf_field
%= csrf_field
Generate hidden input element with CSRF token.
<input name="csrf_token" type="hidden" value="fa6a08..." />
=head2 date_field
%= date_field 'end'
Expand Down
21 changes: 20 additions & 1 deletion lib/Mojolicious/Validator/Validation.pm
Expand Up @@ -5,7 +5,7 @@ use Carp 'croak';
use Scalar::Util 'blessed';

has [qw(input output)] => sub { {} };
has [qw(topic validator)];
has [qw(csrf_token topic validator)];

sub AUTOLOAD {
my $self = shift;
Expand Down Expand Up @@ -39,6 +39,14 @@ sub check {
return $self;
}

sub csrf {
my $self = shift;
my $token = delete $self->input->{csrf_token};
$self->{error}{csrf_token} = ['csrf']
unless $token && $token eq $self->csrf_token;
return $self;
}

sub error { shift->{error}{shift()} }

sub has_data { !!keys %{shift->input} }
Expand Down Expand Up @@ -107,6 +115,13 @@ validation checks.
L<Mojolicious::Validator::Validation> implements the following attributes.
=head2 csrf_token
my $token = $validation->token;
$validation = $validation->token('fa6a08...');
CSRF token.
=head2 input
my $input = $validation->input;
Expand Down Expand Up @@ -147,6 +162,10 @@ and implements the following new ones.
Perform validation check on all values of the current L</"topic">, no more
checks will be performend on them after the first one failed.
=head2 csrf
Check CSRF token.
=head2 error
my $err = $validation->error('foo');
Expand Down
28 changes: 28 additions & 0 deletions t/mojolicious/validation_lite_app.t
Expand Up @@ -24,6 +24,13 @@ any '/' => sub {
$validation->optional('yada')->two;
} => 'index';

any '/csrf' => sub {
my $self = shift;
my $validation = $self->validation->csrf;
return $self->render unless $validation->has_data;
$validation->required('foo');
};

my $t = Test::Mojo->new;

# Required and optional values
Expand Down Expand Up @@ -152,6 +159,20 @@ $t->post_ok('/' => form => {foo => 'no'})->status_is(200)
->element_exists_not('select.field-with-error')
->element_exists_not('input.field-with-error[type="password"]');

# Missing CSRF token
$t->post_ok('/csrf?foo=bar')->status_is(200)
->content_like(qr/Wrong or missing CSRF token!/);

# Correct CSRF token
my $token = $t->ua->get('/csrf')->res->dom->at('[name=csrf_token]')->{value};
$t->post_ok('/csrf' => form => {csrf_token => $token, foo => 'bar'})
->status_is(200)->content_unlike(qr/Wrong or missing CSRF token!/)
->element_exists('[value=bar]');

# Missing CSRF token again
$t->post_ok('/csrf?foo=bar')->status_is(200)
->content_like(qr/Wrong or missing CSRF token!/);

# Failed validation for all fields (with custom helper)
$t->app->helper(
tag_with_error => sub {
Expand Down Expand Up @@ -192,3 +213,10 @@ __DATA__
%= select_field baz => [qw(yada yada)]
%= password_field 'yada'
% end
@@ csrf.html.ep
%= form_for csrf => begin
%= csrf_field
%= 'Wrong or missing CSRF token!' if validation->has_error('csrf_token')
%= text_field 'foo'
%= end

0 comments on commit 18f4e12

Please sign in to comment.