File Coverage

blib/lib/Plack/Middleware/CSRFBlock.pm
Criterion Covered Total %
statement 97 99 97.9
branch 33 40 82.5
condition 18 25 72.0
subroutine 18 18 100.0
pod 2 4 50.0
total 168 186 90.3


line stmt bran cond sub pod time code
1             package Plack::Middleware::CSRFBlock;
2             $Plack::Middleware::CSRFBlock::VERSION = '0.10';
3 2     2   26686 use parent qw(Plack::Middleware);
  2         284  
  2         12  
4 2     2   20537 use strict;
  2         5  
  2         55  
5 2     2   11 use warnings;
  2         5  
  2         53  
6              
7             # ABSTRACT: Block CSRF Attacks with minimal changes to your app
8              
9 2     2   926 use Digest::SHA1;
  2         964  
  2         99  
10 2     2   2040 use Time::HiRes qw(time);
  2         3711  
  2         10  
11 2     2   2463 use HTML::Parser;
  2         14093  
  2         93  
12 2     2   864 use Plack::Request;
  2         73055  
  2         59  
13 2     2   1811 use Plack::TempBuffer;
  2         460  
  2         53  
14 2     2   11 use Plack::Util;
  2         5  
  2         63  
15 2         19 use Plack::Util::Accessor qw(
16             parameter_name header_name add_meta meta_tag token_length
17             session_key blocked onetime _token_generator logger
18 2     2   21 );
  2         5  
19              
20             sub prepare_app {
21 4     4 1 4513 my ($self) = @_;
22              
23 4 100       14 $self->parameter_name('SEC') unless defined $self->parameter_name;
24 4 100       148 $self->token_length(16) unless defined $self->token_length;
25 4 50       39 $self->session_key('csrfblock.token') unless defined $self->session_key;
26              
27             # Upper-case header name and replace - with _
28 4   50     52 my $header_name = uc($self->header_name || 'X-CSRF-Token');
29 4         52 $header_name =~ s/-/_/g;
30 4         14 $self->header_name($header_name);
31              
32             $self->_token_generator(sub {
33 6     6   231 my $token = Digest::SHA1::sha1_hex(rand() . $$ . {} . time);
34 6         33 substr($token, 0 , $self->token_length);
35 4         42 });
36             }
37              
38             sub log {
39 75     75 0 117 my ($self, $level, $msg) = @_;
40              
41 75         360 $self->logger->({ level => $level, message => "CSRFBlock: $msg" });
42             }
43              
44             sub call {
45 50     50 1 175447 my($self, $env) = @_;
46              
47             # cache the logger
48 50 100 50 75   158 $self->logger($env->{'psgix.logger'} || sub { }) unless defined $self->logger;
  75         435  
49              
50             # Generate a Plack Request for this request
51 50         583 my $request = Plack::Request->new($env);
52              
53             # We need a session
54 50         518 my $session = $request->session;
55 50 50       340 unless ($session) {
56 0         0 $self->log( error => 'No session found!' );
57 0 0       0 die "CSRFBlock needs Session." unless $session;
58             }
59              
60 50         164 my $token = $session->{$self->session_key};
61 50 100       331 if($request->method =~ m{^post$}i) {
62             # Log the request with env info
63 28         278 $self->log(debug => 'Got POST Request');
64              
65             # If we don't have a token, can't do anything
66 28 100       121 return $self->token_not_found($env) unless $token;
67              
68 20         71 my $found;
69              
70             # First, check if the header is set correctly.
71 20   100     75 $found = ( $request->header( $self->header_name ) || '') eq $token;
72              
73 20 100       4929 $self->log(debug => 'Found in Header? : ' . ($found ? 1 : 0));
74              
75             # If the token wasn't set, let's check the params
76 20 100       70 unless ($found) {
77 19   50     73 my $val = $request->parameters->{ $self->parameter_name } || '';
78 19         9984 $found = $val eq $token;
79 19 50       86 $self->log(debug => 'Found in parameters : ' . ($found ? 1 : 0));
80             }
81              
82 20 50       65 return $self->token_not_found($env) unless $found;
83              
84             # If we are using onetime token, remove it from the session
85 20 100       70 delete $session->{$self->session_key} if $self->onetime;
86             }
87              
88             return $self->response_cb($self->app->($env), sub {
89 42     42   6747 my $res = shift;
90 42   50     163 my $ct = Plack::Util::header_get($res->[1], 'Content-Type') || '';
91 42 100 66     1108 if($ct !~ m{^text/html}i and $ct !~ m{^application/xhtml[+]xml}i){
92 24         57 return $res;
93             }
94              
95 18         26 my @out;
96 18         68 my $http_host = $request->uri->host;
97 18   66     4861 my $token = $session->{$self->session_key} ||= $self->_token_generator->();
98 18         183 my $parameter_name = $self->parameter_name;
99              
100             my $p = HTML::Parser->new(
101             api_version => 3,
102             start_h => [sub {
103 132         246 my($tag, $attr, $text) = @_;
104 132         193 push @out, $text;
105              
106 2     2   1716 no warnings 'uninitialized';
  2         3  
  2         1213  
107              
108 132         160 $tag = lc($tag);
109             # If we found the head tag and we want to add a tag
110 132 100 100     346 if( $tag eq 'head' && $self->meta_tag) {
111             # Put the csrftoken in a element in
112             # So that you can get the token in javascript in your
113             # App to set in X-CSRF-Token header for all your AJAX
114             # Requests
115 1         13 push @out, q{};
116             }
117              
118             # If tag isn't 'form' and method isn't 'post' we dont care
119 132 100 66     1144 return unless $tag eq 'form' && $attr->{'method'} =~ /post/i;
120              
121 20 100 100     119 if(
122             !($attr->{'action'} =~ m{^https?://([^/:]+)[/:]}
123             and $1 ne $http_host)
124             ) {
125 16         69 push @out, '
126             "name=\"$parameter_name\" value=\"$token\" />";
127             }
128              
129             # TODO: determine xhtml or html?
130 20         174 return;
131 18         294 }, "tagname, attr, text"],
132             default_h => [\@out , '@{text}'],
133             );
134 18         1046 my $done;
135              
136             return sub {
137 36 50       712 return if $done;
138              
139 36 100       87 if(defined(my $chunk = shift)) {
140 18         169 $p->parse($chunk);
141             }
142             else {
143 18         115 $p->eof;
144 18         30 $done++;
145             }
146 36         228 join '', splice @out;
147             }
148 42         361 });
  18         104  
149             }
150              
151             sub token_not_found {
152 8     8 0 17 my ($self, $env) = (shift, shift);
153              
154 8         20 $self->log(error => 'Token not found, returning 403!');
155              
156 8 100       33 if(my $app_for_blocked = $self->blocked) {
157 2         20 return $app_for_blocked->($env, @_);
158             }
159             else {
160 6         63 my $body = 'CSRF detected';
161             return [
162 6         50 403,
163             [ 'Content-Type' => 'text/plain', 'Content-Length' => length($body) ],
164             [ $body ]
165             ];
166             }
167             }
168              
169             1;
170              
171             __END__