File Coverage

blib/lib/Mojolicious/Plugin/CSRFDefender.pm
Criterion Covered Total %
statement 66 66 100.0
branch 16 18 88.8
condition 16 17 94.1
subroutine 11 11 100.0
pod 1 1 100.0
total 110 113 97.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::CSRFDefender;
2              
3 6     6   39355 use strict;
  6         17  
  6         214  
4 6     6   35 use warnings;
  6         9  
  6         184  
5 6     6   34 use Carp;
  6         13  
  6         618  
6              
7             our $VERSION = '0.0.8';
8              
9 6     6   34 use base qw(Mojolicious::Plugin Class::Accessor::Fast);
  6         10  
  6         5625  
10             __PACKAGE__->mk_accessors(qw(
11             parameter_name
12             session_key
13             token_length
14             error_status
15             error_content
16             error_template
17             onetime
18             ));
19              
20 6     6   36003 use String::Random;
  6         18042  
  6         290  
21 6     6   4762 use Path::Class;
  6         205296  
  6         4131  
22              
23             sub register {
24 5     5 1 330 my ($self, $app, $conf) = @_;
25              
26             # Plugin config
27 5   50     25 $conf ||= {};
28              
29             # setting
30 5   100     50 $self->parameter_name($conf->{parameter_name} || 'csrftoken');
31 5   100     120 $self->session_key($conf->{session_key} || 'csrftoken');
32 5   100     60 $self->token_length($conf->{token_length} || 32);
33 5   100     51 $self->error_status($conf->{error_status} || 403);
34 5   100     50 $self->error_content($conf->{error_content} || 'Forbidden');
35 5   100     52 $self->onetime($conf->{onetime} || 0);
36 5 100       35 if ($conf->{error_template}) {
37 1         33 my $file = $app->home->rel_file($conf->{error_template});
38 1         123 $self->error_template($file);
39             }
40              
41             # input check
42             $app->hook(before_dispatch => sub {
43 29     29   753730 my ($c) = @_;
44 29 100       187 unless ($self->_validate_csrf($c)) {
45 9         16 my $content;
46 9 100       54 if ($self->error_template) {
47 2         17 my $file = file($self->error_template);
48 2         471 $content = $file->slurp;
49             }
50             else {
51 7         61 $content = $self->{error_content},
52             }
53 9         869 $c->render(
54             status => $self->{error_status},
55             text => $content,
56             );
57             };
58 5         66 });
59              
60             # output filter
61             $app->hook(after_dispatch => sub {
62 29     29   153954 my ($c) = @_;
63 29         141 my $token = $self->_get_csrf_token($c);
64 29         641 my $p_name = $self->parameter_name;
65 29         196 my $body = $c->res->body;
66 29         6266 $body =~ s{(]*method=["']POST["'][^>]*>)}{$1\n}isg;
67 29         328 $c->res->body($body);
68 5         273 });
69              
70 5         205 return $self;
71             }
72              
73             sub _validate_csrf {
74 29     29   65 my ($self, $c) = @_;
75              
76 29         176 my $p_name = $self->parameter_name;
77 29         291 my $s_name = $self->session_key;
78 29         239 my $request_token = $c->req->param($p_name);
79 29         11937 my $session_token = $c->session($s_name);
80              
81 29 100       590 if ($c->req->method eq 'POST') {
82 15 100       1233 return 0 unless $request_token;
83 6 50       24 return 0 unless $session_token;
84 6 50       28 return 0 unless $request_token eq $session_token;
85             }
86              
87             # onetime
88 20 100 100     1189 if ($c->req->method eq 'POST' && $self->onetime) {
89 2         134 $c->session($self->{session_key} => '');
90             }
91              
92 20         1648 return 1;
93             }
94              
95             sub _get_csrf_token {
96 29     29   224 my ($self, $c) = @_;
97              
98 29         132 my $key = $self->session_key;
99 29         395 my $token = $c->session($key);
100 29         747 my $length = $self->token_length;
101 29 100       247 return $token if $token;
102              
103 7         63 $token = String::Random::random_regex("[a-zA-Z0-9_]{$length}");
104 7         2880 $c->session($key => $token);
105 7         194 return $token;
106             }
107              
108             1;
109              
110             __END__