File Coverage

blib/lib/Mojolicious/Plugin/CSRF.pm
Criterion Covered Total %
statement 81 81 100.0
branch 18 28 64.2
condition 13 26 50.0
subroutine 17 17 100.0
pod 1 1 100.0
total 130 153 84.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::CSRF;
2             # ABSTRACT: Cross Site Request Forgery (CSRF) "prevention" Mojolicious plugin
3              
4 2     2   1562315 use 5.016;
  2         12  
5 2     2   11 use strict;
  2         3  
  2         68  
6 2     2   11 use warnings;
  2         5  
  2         164  
7 2     2   751 use Mojo::Base 'Mojolicious::Plugin';
  2         12879  
  2         16  
8 2     2   2557 use Mojo::DOM;
  2         252889  
  2         81  
9 2     2   16 use Mojo::Util;
  2         4  
  2         1548  
10              
11             our $VERSION = '1.05'; # VERSION
12              
13             sub register {
14 1     1 1 65 my ( $self, $app, $conf ) = @_;
15              
16 1 50       13 my $csrf = Mojolicious::Plugin::CSRF::Base->new( ( ref $conf eq 'HASH' ) ? %$conf : () );
17 1     13   21 $app->helper( csrf => sub { $csrf->c( $_[0] ) } );
  13         10102  
18              
19             $conf->{hooks} //= [
20             before_routes => sub {
21 3     3   104780 my ($c) = @_;
22 3         22 $c->csrf->setup;
23 3         13 $c->csrf->check;
24             },
25             after_render => sub {
26 3     3   76907 my ( $c, $output, $format ) = @_;
27              
28 3 50 33     32 if ( $format eq 'html' and $$output ) {
29 3         12 my $dom = Mojo::DOM->new( Mojo::Util::decode( 'UTF-8', $$output ) );
30 3         61497 my $forms = $dom->find('form[method="post"]');
31              
32 3 100       8299 if ( $forms->size ) {
33             $forms->each( sub {
34 2         77 $_->append_content(
35             '
36             'name="' . $c->csrf->token_name . '" ' .
37             'value="' . $c->csrf->token . '">'
38             );
39 2         37 } );
40 2         675 $$output = Mojo::Util::encode( 'UTF-8', $dom->to_string );
41             }
42             }
43             },
44 1   50     197 ];
45              
46 2         8 $app->hook( shift @{ $conf->{hooks} }, shift @{ $conf->{hooks} } )
  2         12  
47 1   66     8 while ( ref $conf->{hooks} eq 'ARRAY' and @{ $conf->{hooks} } and not @{ $conf->{hooks} } % 2 );
  3   66     54  
  2         8  
48              
49 1         9 return;
50             }
51              
52             package Mojolicious::Plugin::CSRF::Base;
53              
54 2     2   16 use Mojo::Base -base;
  2         4  
  2         14  
55 2     2   1976 use Crypt::URandom;
  2         10930  
  2         2937  
56              
57             has c => undef;
58             has generate_token => sub { sub { unpack( 'H*', Crypt::URandom::urandom(16) ) } };
59             has token_name => 'csrf_token';
60             has header => 'X-CSRF-Token';
61             has methods => sub { [ qw( POST PUT DELETE PATCH ) ] };
62             has include => undef;
63             has exclude => undef;
64              
65             has on_success => sub { sub {
66             my ($c) = @_;
67             $c->log->info('CSRF check success');
68             return 1;
69             } };
70              
71             has on_failure => sub { sub {
72             my ($c) = @_;
73             $c->reply->exception( 'Access Forbidden: CSRF check failure', { status => 403 } );
74             return 0;
75             } };
76              
77             sub setup {
78 3     3   150 my ($self) = @_;
79 3         14 $self->c->res->headers->add( $self->header => $self->token );
80 3         1240 return $self;
81             }
82              
83             sub check {
84 3     3   32 my ($self) = @_;
85              
86 3         10 my $path = $self->c->req->url->path->to_string;
87 3 50       343 return if ( $self->c->app->static->file($path) );
88              
89 3         410 my $method = $self->c->req->method;
90 3 50 33     65 my @methods = @{ ( $self->methods and ref $self->methods eq 'ARRAY' ) ? $self->methods : [] };
  3         13  
91 3 50       70 @methods = 'any' unless @methods;
92 3 100       10 return unless ( grep { uc $_ eq $method or lc $_ eq 'any' } @methods );
  12 100       57  
93              
94 2 50 33     13 if ( $self->include and ref $self->include eq 'ARRAY' ) {
95 2         72 my $include = 0;
96 2         6 for ( @{ $self->include } ) {
  2         9  
97 2 50       45 if ( $path =~ $_ ) {
98 2         7 $include = 1;
99 2         5 last;
100             }
101             }
102 2 50       9 return unless $include;
103             }
104              
105 2 50 33     10 if ( $self->exclude and ref $self->exclude eq 'ARRAY' ) {
106 2         55 for ( @{ $self->exclude } ) {
  2         10  
107 2 50       32 return if ( $path =~ $_ );
108             }
109             }
110              
111 2         10 my $session = $self->c->session( $self->token_name );
112 2         67 my $param = $self->c->param( $self->token_name );
113 2         789 my $header = $self->c->req->headers->header( $self->header );
114 2 100 66     142 my $on = 'on_' . ( (
115             not $session or
116             not (
117             $param and $param eq $session or
118             $header and $header eq $session
119             )
120             ) ? 'failure' : 'success' );
121              
122 2         8 return $self->$on->( $self->c );
123             }
124              
125             sub token {
126 7     7   839 my ($self) = @_;
127             return
128 7   66     22 $self->c->session( $self->token_name ) ||
129             $self->c->session( $self->token_name => $self->generate_token->() )->session( $self->token_name );
130             }
131              
132             sub url_for {
133 2     2   22 my ( $self, @params ) = @_;
134 2         8 return $self->c->url_for(@params)->query( { $self->token_name => $self->token } );
135             }
136              
137             sub delete_token {
138 1     1   24 my ($self) = @_;
139 1         4 delete $self->c->session->{ $self->token_name };
140 1         195 return $self;
141             }
142              
143             1;
144              
145             __END__