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__ |