File Coverage

blib/lib/Ark/Plugin/CSRFDefender.pm
Criterion Covered Total %
statement 37 37 100.0
branch 13 14 92.8
condition 3 6 50.0
subroutine 8 8 100.0
pod 2 3 66.6
total 63 68 92.6


line stmt bran cond sub pod time code
1             package Ark::Plugin::CSRFDefender;
2 4     4   2556 use strict;
  4         12  
  4         135  
3 4     4   22 use warnings;
  4         12  
  4         97  
4 4     4   21 use Ark::Plugin;
  4         9  
  4         24  
5 4     4   3758 use Data::UUID;
  4         4522  
  4         4816  
6              
7             has csrf_defender_param_name => (
8             is => 'ro',
9             isa => 'Str',
10             lazy => 1,
11             default => sub {
12             shift->class_config->{param_name} || 'csrf_token';
13             },
14             );
15              
16             has csrf_defender_session_name => (
17             is => 'ro',
18             isa => 'Str',
19             lazy => 1,
20             default => sub {
21             my $self = shift;
22             $self->class_config->{session_name} || $self->csrf_defender_param_name;
23             },
24             );
25              
26             has csrf_defender_validate_only => (
27             is => 'ro',
28             isa => 'Bool',
29             lazy => 1,
30             default => sub {
31             my $self = shift;
32             exists $self->class_config->{validate_only} ? $self->class_config->{validate_only} : undef;
33             },
34             );
35              
36             has csrf_defender_error_output => (
37             is => 'ro',
38             isa => 'Str',
39             lazy => 1,
40             default => sub {
41             shift->class_config->{error_output} || <<'...';
42            
43            
44            
45             403 Forbidden
46            
47            
48            

403 Forbidden

49            

50             Session validation failed.
51            

52            
53            
54             ...
55             }
56             );
57              
58             has csrf_defender_error_code => (
59             is => 'ro',
60             isa => 'Str',
61             lazy => 1,
62             default => sub {
63             shift->class_config->{error_code} || 403;
64             }
65             );
66              
67             has csrf_defender_error_action => (
68             is => 'ro',
69             isa => 'Str',
70             lazy => 1,
71             default => sub {
72             shift->class_config->{error_action} || '';
73             }
74             );
75              
76             has csrf_defender_filter_form => (
77             is => 'ro',
78             isa => 'Bool',
79             lazy => 1,
80             default => sub {
81             shift->class_config->{filter_form} || undef;
82             },
83             );
84              
85             my $uuid = Data::UUID->new;
86             has csrf_token => (
87             is => 'ro',
88             isa => 'Str',
89             lazy => 1,
90             default => sub {
91             my $c = shift;
92              
93             if (my $token = $c->session->get($c->csrf_defender_session_name)) {
94             return $token;
95             }
96             else {
97             my $token = $uuid->create_str;
98             $c->session->set($c->csrf_defender_session_name => $token);
99              
100             return $token;
101             }
102             },
103             predicate => '_has_csrf_token',
104             );
105              
106             sub validate_csrf_token {
107 71     71 1 209 my $c = shift;
108 71         259 my $req = $c->request;
109 71 100       173 if ($c->_is_csrf_validation_needed) {
110 48         359 my $param_token = $req->param($c->csrf_defender_param_name);
111 48         19900 my $session_token = $c->csrf_token;
112 48 100 33     376 if (!$param_token || !$session_token || ($param_token ne $session_token)) {
      66        
113 27         145 return (); # bad
114             }
115             }
116 44         190 return 1; # good
117             }
118              
119             sub forward_csrf_error {
120 12     12 0 31 my $c = shift;
121              
122 12 100       76 if ($c->csrf_defender_error_action) {
123 3         23 $c->res->code($c->csrf_defender_error_code);
124 3         32 $c->forward($c->csrf_defender_error_action);
125             }
126             else {
127 9         55 $c->res->code($c->csrf_defender_error_code);
128 9         80 $c->res->body($c->csrf_defender_error_output);
129 9         62 $c->res->header('Content-Type', 'text/html; charset=UTF-8');
130             }
131             }
132              
133             sub _is_csrf_validation_needed {
134 71     71   86 my $c = shift;
135 71         395 my $method = $c->req->method;
136 71 50       491 return () if !$method;
137              
138             return
139 71 100       346 $method eq 'POST' ? 1 :
    100          
    100          
140             $method eq 'PUT' ? 1 :
141             $method eq 'DELETE' ? 1 : ();
142             }
143              
144             sub html_filter_for_csrf {
145 14     14 1 24 my ($c, $html) = @_;
146              
147 14         45 my $reg = qr//i;
148 14         209 $html =~ s!($reg)!$1\n!isg;
  9         43  
  9         40  
149              
150 14         54 $html;
151             }
152              
153             after finalize_body => sub {
154             my $c = shift;
155              
156             return if $c->res->binary;
157             my $html = $c->res->body or return;
158             return unless $c->csrf_defender_filter_form;
159              
160             $html = $c->html_filter_for_csrf($html);
161             $c->res->body($html);
162             };
163              
164             around dispatch => sub {
165             my $orig = shift;
166             my ($c) = @_;
167              
168             # surely asign csrf_token
169             $c->csrf_token;
170             if (!$c->csrf_defender_validate_only && !$c->validate_csrf_token) {
171             $c->forward_csrf_error;
172             }
173             else {
174             $orig->(@_);
175             }
176             };
177              
178             1;
179             __END__