line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Plack::Middleware::XSRFBlock; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
{ |
4
|
|
|
|
|
|
|
$Plack::Middleware::XSRFBlock::DIST = 'Plack-Middleware-XSRFBlock'; |
5
|
|
|
|
|
|
|
} |
6
|
|
|
|
|
|
|
$Plack::Middleware::XSRFBlock::VERSION = '0.0.19'; |
7
|
9
|
|
|
9
|
|
132676
|
use strict; |
|
9
|
|
|
|
|
28
|
|
|
9
|
|
|
|
|
275
|
|
8
|
9
|
|
|
9
|
|
51
|
use warnings; |
|
9
|
|
|
|
|
18
|
|
|
9
|
|
|
|
|
240
|
|
9
|
9
|
|
|
9
|
|
575
|
use parent 'Plack::Middleware'; |
|
9
|
|
|
|
|
341
|
|
|
9
|
|
|
|
|
59
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
|
12
|
9
|
|
|
9
|
|
18190
|
use Digest::HMAC_SHA1 'hmac_sha1_hex'; |
|
9
|
|
|
|
|
17449
|
|
|
9
|
|
|
|
|
533
|
|
13
|
9
|
|
|
9
|
|
610
|
use HTTP::Status qw(:constants); |
|
9
|
|
|
|
|
5005
|
|
|
9
|
|
|
|
|
3960
|
|
14
|
|
|
|
|
|
|
|
15
|
9
|
|
|
9
|
|
571
|
use Plack::Request; |
|
9
|
|
|
|
|
77297
|
|
|
9
|
|
|
|
|
262
|
|
16
|
9
|
|
|
9
|
|
4311
|
use Plack::Response; |
|
9
|
|
|
|
|
11240
|
|
|
9
|
|
|
|
|
265
|
|
17
|
9
|
|
|
9
|
|
67
|
use Plack::Util; |
|
9
|
|
|
|
|
22
|
|
|
9
|
|
|
|
|
438
|
|
18
|
9
|
|
|
|
|
47
|
use Plack::Util::Accessor qw( |
19
|
|
|
|
|
|
|
blocked |
20
|
|
|
|
|
|
|
cookie_expiry_seconds |
21
|
|
|
|
|
|
|
cookie_name |
22
|
|
|
|
|
|
|
cookie_is_session_cookie |
23
|
|
|
|
|
|
|
cookie_options |
24
|
|
|
|
|
|
|
http_method_regex |
25
|
|
|
|
|
|
|
contents_to_filter_regex |
26
|
|
|
|
|
|
|
inject_form_input |
27
|
|
|
|
|
|
|
logger |
28
|
|
|
|
|
|
|
meta_tag |
29
|
|
|
|
|
|
|
token_per_request |
30
|
|
|
|
|
|
|
parameter_name |
31
|
|
|
|
|
|
|
header_name |
32
|
|
|
|
|
|
|
secret |
33
|
9
|
|
|
9
|
|
58
|
); |
|
9
|
|
|
|
|
19
|
|
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
sub prepare_app { |
36
|
88
|
|
|
88
|
1
|
13133
|
my $self = shift; |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
# this needs a value if we aren't given one |
39
|
88
|
|
50
|
|
|
220
|
$self->parameter_name( $self->parameter_name || 'xsrf_token' ); |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# default to 1 so we inject hidden inputs to forms |
42
|
88
|
50
|
|
|
|
1344
|
$self->inject_form_input(1) unless defined $self->inject_form_input; |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# match methods |
45
|
88
|
|
33
|
|
|
771
|
$self->http_method_regex( $self->http_method_regex || qr{^post$}i ); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# match content types |
48
|
88
|
|
33
|
|
|
1026
|
$self->contents_to_filter_regex( |
49
|
|
|
|
|
|
|
$self->contents_to_filter_regex || |
50
|
|
|
|
|
|
|
qr{^(?: (?:text/html) | (?:application/xhtml(?:\+xml)?) )\b}ix, |
51
|
|
|
|
|
|
|
); |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
# store the cookie_name |
54
|
88
|
|
50
|
|
|
1130
|
$self->cookie_name( $self->cookie_name || 'PSGI-XSRF-Token' ); |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
# cookie is session cookie |
57
|
88
|
|
50
|
|
|
898
|
$self->cookie_is_session_cookie( $self->cookie_is_session_cookie || 0 ); |
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
# extra optional options for the cookie |
60
|
88
|
|
100
|
|
|
892
|
$self->cookie_options( $self->cookie_options || {} ); |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
# default to one token per session, not one per request |
63
|
88
|
100
|
|
|
|
843
|
my $token_per_request = $self->token_per_request ? 1 : 0; |
64
|
|
|
|
|
|
|
$self->token_per_request( |
65
|
|
|
|
|
|
|
ref $self->token_per_request eq 'CODE' |
66
|
|
|
|
|
|
|
? $self->token_per_request |
67
|
4
|
|
|
4
|
|
35
|
: sub { $token_per_request } |
68
|
88
|
100
|
|
|
|
410
|
); |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# default to a cookie life of three hours |
71
|
88
|
|
50
|
|
|
969
|
$self->cookie_expiry_seconds( $self->cookie_expiry_seconds || (3 * 60 * 60) ); |
72
|
|
|
|
|
|
|
} |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
sub detect_xsrf { |
76
|
13
|
|
|
13
|
1
|
21
|
my $self = shift; |
77
|
13
|
|
|
|
|
24
|
my $request = shift; |
78
|
13
|
|
|
|
|
17
|
my $env = shift; |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
# X- header takes precedence over form fields |
81
|
13
|
|
|
|
|
22
|
my $val; |
82
|
13
|
100
|
|
|
|
46
|
$val = $request->header( $self->header_name ) |
83
|
|
|
|
|
|
|
if (defined $self->header_name); |
84
|
|
|
|
|
|
|
# fallback to the parameter value |
85
|
13
|
|
100
|
|
|
657
|
$val ||= $request->parameters->{ $self->parameter_name }; |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# it's not easy to decide if we're missing the X- value or the form |
88
|
|
|
|
|
|
|
# value |
89
|
|
|
|
|
|
|
# We can say for certain that if we don't have the header_name set |
90
|
|
|
|
|
|
|
# it's a missing form parameter |
91
|
|
|
|
|
|
|
# If it is set ... well, either could be missing |
92
|
13
|
100
|
100
|
|
|
3626
|
if (!defined $val || !length $val) { |
93
|
|
|
|
|
|
|
# no X- headers expected |
94
|
7
|
100
|
|
|
|
18
|
return 'form field missing' |
95
|
|
|
|
|
|
|
if not defined $self->header_name; |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
# X- headers and form data allowed |
98
|
1
|
|
|
|
|
17
|
return 'xsrf token missing'; |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
} |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
# grab the cookie where we store the token |
103
|
6
|
|
|
|
|
29
|
my $cookie_value = $request->cookies->{$self->cookie_name}; |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
# get the value we expect from the cookie |
106
|
6
|
50
|
|
|
|
501
|
return 'cookie missing' |
107
|
|
|
|
|
|
|
unless defined $cookie_value; |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
# reject if the form value and the token don't match |
110
|
6
|
100
|
|
|
|
21
|
return 'invalid token' |
111
|
|
|
|
|
|
|
if $val ne $cookie_value; |
112
|
|
|
|
|
|
|
|
113
|
3
|
50
|
|
|
|
13
|
return 'invalid signature' |
114
|
|
|
|
|
|
|
if $self->invalid_signature($val); |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
# No XSRF detected |
117
|
3
|
|
|
|
|
29
|
return; |
118
|
|
|
|
|
|
|
} |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
sub call { |
121
|
40
|
|
|
40
|
1
|
355492
|
my $self = shift; |
122
|
40
|
|
|
|
|
71
|
my $env = shift; |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
# cache the logger |
125
|
|
|
|
23
|
|
|
$self->logger($env->{'psgix.logger'} || sub { }) |
126
|
40
|
100
|
50
|
|
|
148
|
unless defined $self->logger; |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
# we'll need the Plack::Request for this request |
129
|
40
|
|
|
|
|
724
|
my $request = Plack::Request->new($env); |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
# deal with form posts |
132
|
40
|
100
|
|
|
|
461
|
if ($request->method =~ $self->http_method_regex) { |
133
|
13
|
|
|
|
|
215
|
$self->log(info => 'form submitted'); |
134
|
|
|
|
|
|
|
|
135
|
13
|
|
|
|
|
44
|
my $msg = $self->detect_xsrf($request, $env); |
136
|
13
|
100
|
|
|
|
86
|
return $self->xsrf_detected({ env => $env, msg => $msg }) |
137
|
|
|
|
|
|
|
if defined $msg; |
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
|
140
|
30
|
|
|
|
|
505
|
return $self->filter_response($request, $env); |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
|
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
sub should_be_filtered { |
145
|
30
|
|
|
30
|
1
|
77
|
my ($self, $request, $env, $res) = @_; |
146
|
|
|
|
|
|
|
|
147
|
30
|
|
|
|
|
130
|
my $headers = Plack::Util::headers($res->[1]); |
148
|
30
|
|
50
|
|
|
953
|
my $ct = $headers->get('Content-Type') || ''; |
149
|
30
|
|
|
|
|
1095
|
return !! ($ct =~ $self->contents_to_filter_regex); |
150
|
|
|
|
|
|
|
} |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
sub generate_token { |
154
|
25
|
|
|
25
|
1
|
72
|
my ($self, $request, $env, $res) = @_; |
155
|
|
|
|
|
|
|
|
156
|
25
|
|
|
|
|
123
|
my $token = $request->cookies->{$self->cookie_name}; |
157
|
|
|
|
|
|
|
|
158
|
25
|
100
|
100
|
|
|
1280
|
return $token if $token && !$self->token_per_request->( $self, $request, $env ); |
159
|
|
|
|
|
|
|
|
160
|
21
|
|
|
|
|
349
|
my $data = rand() . $$ . {} . time; |
161
|
21
|
|
|
|
|
159
|
my $key = "@INC"; |
162
|
21
|
|
|
|
|
88
|
$token = hmac_sha1_hex($data, $key); |
163
|
|
|
|
|
|
|
|
164
|
21
|
50
|
|
|
|
847
|
if (defined $self->secret) { |
165
|
0
|
|
|
|
|
0
|
my $sig = hmac_sha1_hex($token, $self->secret); |
166
|
0
|
|
|
|
|
0
|
$token .= "--$sig"; |
167
|
|
|
|
|
|
|
} |
168
|
|
|
|
|
|
|
|
169
|
21
|
|
|
|
|
152
|
return $token; |
170
|
|
|
|
|
|
|
} |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
sub cookie_handler { |
174
|
25
|
|
|
25
|
1
|
74
|
my ($self, $request, $env, $res, $token) = @_; |
175
|
|
|
|
|
|
|
|
176
|
25
|
|
|
|
|
43
|
my %cookie_expires; |
177
|
25
|
50
|
|
|
|
83
|
unless ( $self->cookie_is_session_cookie ) { |
178
|
25
|
|
|
|
|
157
|
$cookie_expires{expires} = time + $self->cookie_expiry_seconds; |
179
|
|
|
|
|
|
|
} |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
# we need to add our cookie |
182
|
|
|
|
|
|
|
$self->_set_cookie( |
183
|
25
|
|
|
|
|
209
|
$token, |
184
|
|
|
|
|
|
|
$res, |
185
|
|
|
|
|
|
|
path => '/', |
186
|
|
|
|
|
|
|
%cookie_expires, |
187
|
|
|
|
|
|
|
); |
188
|
|
|
|
|
|
|
|
189
|
25
|
|
|
|
|
70
|
return; |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
sub filter_response_html { |
194
|
25
|
|
|
25
|
1
|
77
|
my ($self, $request, $env, $res, $token) = @_; |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# Do not load these unless HTML filter is used |
197
|
25
|
|
|
|
|
4576
|
require HTML::Parser; |
198
|
25
|
|
|
|
|
45584
|
require HTML::Escape; |
199
|
25
|
|
|
|
|
5060
|
import HTML::Escape qw(escape_html); |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# escape token (someone might have tampered with the cookie) |
202
|
25
|
|
|
|
|
157
|
$token = escape_html($token); |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
# let's inject our field+token into the form |
205
|
25
|
|
|
|
|
51
|
my @out; |
206
|
25
|
|
|
|
|
151
|
my $http_host = $request->uri->host; |
207
|
25
|
|
|
|
|
8271
|
my $parameter_name = $self->parameter_name; |
208
|
|
|
|
|
|
|
|
209
|
25
|
|
|
|
|
587
|
my $p = HTML::Parser->new( api_version => 3 ); |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
$p->handler(default => [\@out , '@{text}']), |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
# we need *all* tags, otherwise we end up with gibberish as the final |
214
|
|
|
|
|
|
|
# page output |
215
|
|
|
|
|
|
|
# i.e. unless there's a better way, we *can not* do |
216
|
|
|
|
|
|
|
# $p->report_tags(qw/head form/); |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
# inject our xSRF information |
219
|
|
|
|
|
|
|
$p->handler( |
220
|
|
|
|
|
|
|
start => sub { |
221
|
175
|
|
|
175
|
|
383
|
my($tag, $attr, $text) = @_; |
222
|
|
|
|
|
|
|
# we never want to throw anything away |
223
|
175
|
|
|
|
|
294
|
push @out, $text; |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
# for easier comparison |
226
|
175
|
|
|
|
|
272
|
$tag = lc($tag); |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
# If we found the head tag and we want to add a <meta> tag |
229
|
175
|
100
|
100
|
|
|
470
|
if( $tag eq 'head' && $self->meta_tag) { |
230
|
|
|
|
|
|
|
# Put the csrftoken in a <meta> element in <head> |
231
|
|
|
|
|
|
|
# So that you can get the token in javascript in your |
232
|
|
|
|
|
|
|
# App to set in X-CSRF-Token header for all your AJAX |
233
|
|
|
|
|
|
|
# Requests |
234
|
4
|
|
|
|
|
41
|
push @out, |
235
|
|
|
|
|
|
|
sprintf( |
236
|
|
|
|
|
|
|
q{<meta name="%s" content="%s"/>}, |
237
|
|
|
|
|
|
|
$self->meta_tag, |
238
|
|
|
|
|
|
|
$token |
239
|
|
|
|
|
|
|
); |
240
|
|
|
|
|
|
|
} |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# If tag isn't 'form' and method isn't matched, we dont care |
243
|
|
|
|
|
|
|
return unless |
244
|
|
|
|
|
|
|
defined $tag |
245
|
|
|
|
|
|
|
&& defined $attr->{'method'} |
246
|
|
|
|
|
|
|
&& $tag eq 'form' |
247
|
175
|
50
|
66
|
|
|
1256
|
&& $attr->{'method'} =~ $self->http_method_regex; |
|
|
|
66
|
|
|
|
|
|
|
|
66
|
|
|
|
|
248
|
|
|
|
|
|
|
|
249
|
25
|
0
|
33
|
|
|
492
|
if( |
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
250
|
|
|
|
|
|
|
!( |
251
|
|
|
|
|
|
|
defined $attr |
252
|
|
|
|
|
|
|
and |
253
|
|
|
|
|
|
|
exists $attr->{'action'} |
254
|
|
|
|
|
|
|
and |
255
|
|
|
|
|
|
|
$attr->{'action'} =~ m{^https?://([^/:]+)[/:]} |
256
|
|
|
|
|
|
|
and |
257
|
|
|
|
|
|
|
defined $http_host |
258
|
|
|
|
|
|
|
and |
259
|
|
|
|
|
|
|
$1 ne $http_host |
260
|
|
|
|
|
|
|
) |
261
|
|
|
|
|
|
|
) { |
262
|
25
|
|
|
|
|
181
|
push @out, |
263
|
|
|
|
|
|
|
sprintf( |
264
|
|
|
|
|
|
|
'<input type="hidden" name="%s" value="%s" />', |
265
|
|
|
|
|
|
|
$parameter_name, |
266
|
|
|
|
|
|
|
$token |
267
|
|
|
|
|
|
|
); |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
# TODO: determine xhtml or html? |
271
|
25
|
|
|
|
|
132
|
return; |
272
|
|
|
|
|
|
|
}, |
273
|
25
|
|
|
|
|
1044
|
"tagname, attr, text", |
274
|
|
|
|
|
|
|
); |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
# we never want to throw anything away |
277
|
|
|
|
|
|
|
$p->handler( |
278
|
|
|
|
|
|
|
default => sub { |
279
|
425
|
|
|
425
|
|
907
|
my($tag, $attr, $text) = @_; |
280
|
425
|
|
|
|
|
1739
|
push @out, $text; |
281
|
|
|
|
|
|
|
}, |
282
|
25
|
|
|
|
|
154
|
"tagname, attr, text", |
283
|
|
|
|
|
|
|
); |
284
|
|
|
|
|
|
|
|
285
|
25
|
|
|
|
|
56
|
my $done; |
286
|
|
|
|
|
|
|
return sub { |
287
|
50
|
50
|
|
50
|
|
1119
|
return if $done; |
288
|
|
|
|
|
|
|
|
289
|
50
|
100
|
|
|
|
135
|
if(defined(my $chunk = shift)) { |
290
|
25
|
|
|
|
|
169
|
$p->parse($chunk); |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
else { |
293
|
25
|
|
|
|
|
148
|
$p->eof; |
294
|
25
|
|
|
|
|
70
|
$done++; |
295
|
|
|
|
|
|
|
} |
296
|
50
|
|
|
|
|
306
|
join '', splice @out; |
297
|
|
|
|
|
|
|
} |
298
|
25
|
|
|
|
|
151
|
} |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
sub filter_response { |
302
|
30
|
|
|
30
|
1
|
79
|
my ($self, $request, $env) = @_; |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
return Plack::Util::response_cb($self->app->($env), sub { |
305
|
30
|
|
|
30
|
|
5821
|
my $res = shift; |
306
|
|
|
|
|
|
|
|
307
|
30
|
100
|
|
|
|
110
|
return $res unless $self->should_be_filtered($request, $env, $res); |
308
|
|
|
|
|
|
|
|
309
|
25
|
|
|
|
|
623
|
my $token = $self->generate_token($request, $env, $res); |
310
|
|
|
|
|
|
|
|
311
|
25
|
|
|
|
|
130
|
$self->cookie_handler($request, $env, $res, $token); |
312
|
|
|
|
|
|
|
|
313
|
25
|
50
|
|
|
|
78
|
return $res unless $self->inject_form_input; |
314
|
|
|
|
|
|
|
|
315
|
25
|
|
|
|
|
175
|
return $self->filter_response_html($request, $env, $res, $token); |
316
|
30
|
|
|
|
|
131
|
}); |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
sub invalid_signature { |
321
|
3
|
|
|
3
|
1
|
8
|
my ($self, $value) = @_; |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
# we dont use signed cookies |
324
|
3
|
50
|
|
|
|
17
|
return 0 if !defined $self->secret; |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
# cookie isn't signed |
327
|
0
|
|
|
|
|
0
|
my ($token, $signature) = split /--/, $value; |
328
|
0
|
0
|
0
|
|
|
0
|
return 1 if !defined $signature || $signature eq ''; |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
# signature doesn't validate |
331
|
0
|
|
|
|
|
0
|
return hmac_sha1_hex($token, $self->secret) ne $signature; |
332
|
|
|
|
|
|
|
} |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
sub xsrf_detected { |
336
|
10
|
|
|
10
|
1
|
20
|
my $self = shift; |
337
|
10
|
|
|
|
|
17
|
my $args = shift; |
338
|
10
|
|
|
|
|
17
|
my $env = $args->{env}; |
339
|
|
|
|
|
|
|
my $msg = $args->{msg} |
340
|
|
|
|
|
|
|
? sprintf('XSRF detected [%s]', $args->{msg}) |
341
|
10
|
50
|
|
|
|
53
|
: 'XSRF detected'; |
342
|
|
|
|
|
|
|
|
343
|
10
|
100
|
|
|
|
30
|
if (my $app_for_blocked = $self->blocked) { |
344
|
1
|
|
|
|
|
9
|
$self->log(info => "$msg, invoking `blocked` coderef"); |
345
|
1
|
|
|
|
|
5
|
return $app_for_blocked->($env, $msg, app => $self->app); |
346
|
|
|
|
|
|
|
} |
347
|
|
|
|
|
|
|
|
348
|
9
|
|
|
|
|
63
|
$self->log(error => "$msg, returning HTTP_FORBIDDEN"); |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
return [ |
351
|
9
|
|
|
|
|
141
|
HTTP_FORBIDDEN, |
352
|
|
|
|
|
|
|
[ 'Content-Type' => 'text/plain', 'Content-Length' => length($msg) ], |
353
|
|
|
|
|
|
|
[ $msg ] |
354
|
|
|
|
|
|
|
]; |
355
|
|
|
|
|
|
|
} |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
sub log { |
359
|
23
|
|
|
23
|
1
|
54
|
my ($self, $level, $msg) = @_; |
360
|
23
|
|
|
|
|
111
|
$self->logger->({ level => $level, message => "XSRFBlock: $msg" }); |
361
|
|
|
|
|
|
|
} |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
# taken from Plack::Session::State::Cookie |
364
|
|
|
|
|
|
|
# there's a very good reason why we have to do the cookie setting this way ... |
365
|
|
|
|
|
|
|
# I just can't explain it clearly right now |
366
|
|
|
|
|
|
|
sub _set_cookie { |
367
|
25
|
|
|
25
|
|
109
|
my($self, $id, $res, %options) = @_; |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
# TODO: Do not use Plack::Response |
370
|
25
|
|
|
|
|
168
|
my $response = Plack::Response->new(@$res); |
371
|
|
|
|
|
|
|
$response->cookies->{ $self->cookie_name } = +{ |
372
|
|
|
|
|
|
|
value => $id, |
373
|
|
|
|
|
|
|
%options, |
374
|
25
|
|
|
|
|
2365
|
%{ $self->cookie_options }, |
|
25
|
|
|
|
|
70
|
|
375
|
|
|
|
|
|
|
}; |
376
|
|
|
|
|
|
|
|
377
|
25
|
|
|
|
|
506
|
my $final_r = $response->finalize; |
378
|
25
|
|
|
|
|
4886
|
$res->[1] = $final_r->[1]; # headers |
379
|
|
|
|
|
|
|
} |
380
|
|
|
|
|
|
|
|
381
|
|
|
|
|
|
|
1; |
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
# ABSTRACT: Block XSRF Attacks with minimal changes to your app |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
=pod |
388
|
|
|
|
|
|
|
|
389
|
|
|
|
|
|
|
=encoding UTF-8 |
390
|
|
|
|
|
|
|
|
391
|
|
|
|
|
|
|
=head1 NAME |
392
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
Plack::Middleware::XSRFBlock - Block XSRF Attacks with minimal changes to your app |
394
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
=head1 VERSION |
396
|
|
|
|
|
|
|
|
397
|
|
|
|
|
|
|
version 0.0.19 |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
=head1 SYNOPSIS |
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
The simplest way to use the plugin is: |
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
use Plack::Builder; |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
my $app = sub { ... }; |
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
builder { |
408
|
|
|
|
|
|
|
enable 'XSRFBlock'; |
409
|
|
|
|
|
|
|
$app; |
410
|
|
|
|
|
|
|
} |
411
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
You may also over-ride any, or all of these values: |
413
|
|
|
|
|
|
|
|
414
|
|
|
|
|
|
|
builder { |
415
|
|
|
|
|
|
|
enable 'XSRFBlock', |
416
|
|
|
|
|
|
|
parameter_name => 'xsrf_token', |
417
|
|
|
|
|
|
|
cookie_name => 'PSGI-XSRF-Token', |
418
|
|
|
|
|
|
|
cookie_options => {}, |
419
|
|
|
|
|
|
|
cookie_expiry_seconds => (3 * 60 * 60), |
420
|
|
|
|
|
|
|
token_per_request => 0, |
421
|
|
|
|
|
|
|
meta_tag => undef, |
422
|
|
|
|
|
|
|
inject_form_input => 1, |
423
|
|
|
|
|
|
|
header_name => undef, |
424
|
|
|
|
|
|
|
secret => undef, |
425
|
|
|
|
|
|
|
http_method_regex => qr{^post$}i, |
426
|
|
|
|
|
|
|
contents_to_filter_regex => qr{^(text/html|application/xhtml(?:\+xml)?)\b}i, |
427
|
|
|
|
|
|
|
blocked => sub { |
428
|
|
|
|
|
|
|
return [ $status, $headers, $body ] |
429
|
|
|
|
|
|
|
}, |
430
|
|
|
|
|
|
|
; |
431
|
|
|
|
|
|
|
$app; |
432
|
|
|
|
|
|
|
} |
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
=head1 DESCRIPTION |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
This middleware blocks XSRF. You can use this middleware without any |
437
|
|
|
|
|
|
|
modifications to your application. |
438
|
|
|
|
|
|
|
|
439
|
|
|
|
|
|
|
=head1 OPTIONS |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
=over 4 |
442
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
=item parameter_name (default: 'xsrf_token') |
444
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
The name assigned to the hidden form input containing the token. |
446
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
=item cookie_name (default: 'PSGI-XSRF-Token') |
448
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
The name of the cookie used to store the token value. |
450
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
=item cookie_expiry_seconds (default: 3*60*60) |
452
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
The expiration time in seconds of the XSRF token |
454
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
=item cookie_is_session_cookie (default: 0) |
456
|
|
|
|
|
|
|
|
457
|
|
|
|
|
|
|
If set to a true value, the XSRF token cookie will be set as a session cookie |
458
|
|
|
|
|
|
|
and C<cookie_expiry_seconds> will be ignored. |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
=item cookie_options (default: {}) |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
Extra cookie options to be set with the cookie. This is useful for things like |
463
|
|
|
|
|
|
|
setting C<HttpOnly> to tell the browser to only send it with HTTP requests, |
464
|
|
|
|
|
|
|
and C<Secure> on the cookie to force the cookie to only be sent on SSL requests. |
465
|
|
|
|
|
|
|
|
466
|
|
|
|
|
|
|
builder { |
467
|
|
|
|
|
|
|
enable 'XSRFBlock', cookie_options => { secure => 1, httponly => 1 }; |
468
|
|
|
|
|
|
|
} |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
=item token_per_request (default: 0) |
471
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
If this is true a new token is assigned for each request made (but see below). |
473
|
|
|
|
|
|
|
|
474
|
|
|
|
|
|
|
This may make your application more secure, but more susceptible to |
475
|
|
|
|
|
|
|
double-submit issues. |
476
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
If this is a coderef, the coderef will be evaluated with the following arguments: |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
=item http_method_regex (default: qr{^post$}i) |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
Which HTTP methods to check. Can be useful to also handle PUT, DELETE, |
482
|
|
|
|
|
|
|
PATCH, and the like. |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
=item contents_to_filter_regex default: qr{^(text/html|application/xhtml(?:\+xml)?)\b}i) |
485
|
|
|
|
|
|
|
|
486
|
|
|
|
|
|
|
Only modify <form> elements in responses whose content type matches this regex |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
=over |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
=item * The middleware object itself, |
491
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
=item * The request, |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
=item * The environment |
495
|
|
|
|
|
|
|
|
496
|
|
|
|
|
|
|
=back |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
If the result of the evaluation is a true value, a new token will be assigned. |
499
|
|
|
|
|
|
|
This allows fine-grained control, for example to avoid assigning new tokens when |
500
|
|
|
|
|
|
|
incidental requests are made (e.g. on-page ajax requests). |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
=item meta_tag (default: undef) |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
If this is set, use the value as the name of the meta tag to add to the head |
505
|
|
|
|
|
|
|
section of output pages. |
506
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
This is useful when you are using javascript that requires access to the token |
508
|
|
|
|
|
|
|
value for making AJAX requests. |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
=item inject_form_input (default: 1) |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
If this is unset, hidden inputs will not be injected into your forms, and no |
513
|
|
|
|
|
|
|
HTML parsing will be done on the page responses. |
514
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
This can be useful if you only do AJAX requests, and can utilize headers |
516
|
|
|
|
|
|
|
and/or cookies instead, and not need the extra overhead of processing |
517
|
|
|
|
|
|
|
the HTML document every time. |
518
|
|
|
|
|
|
|
|
519
|
|
|
|
|
|
|
=item header_name (default: undef) |
520
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
If this is set, use the value as the name of the response heaer that the token |
522
|
|
|
|
|
|
|
can be sent in. This is useful for non-browser based submissions; e.g. |
523
|
|
|
|
|
|
|
Javascript AJAX requests. |
524
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
=item secret (default: undef) |
526
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
Signs the cookie with supplied secret (if set). |
528
|
|
|
|
|
|
|
|
529
|
|
|
|
|
|
|
=item blocked (default: undef) |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
If this is set it should be a PSGI application that is returned instead of the |
532
|
|
|
|
|
|
|
default HTTP_FORBIDDEN(403) and text/plain response. |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
This could be useful if you'd like to perform some action that's more in |
535
|
|
|
|
|
|
|
keeping with your application - e.g. return a styled error page. |
536
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
=back |
538
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
=head1 ERRORS |
540
|
|
|
|
|
|
|
|
541
|
|
|
|
|
|
|
The module emits various errors based on the cause of the XSRF detected. The |
542
|
|
|
|
|
|
|
messages will be of the form C<XSRF detected [reason]> |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
=over 4 |
545
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
=item form field missing |
547
|
|
|
|
|
|
|
|
548
|
|
|
|
|
|
|
The request was submitted but there was no value submitted in the form field |
549
|
|
|
|
|
|
|
specified by <C$self->parameter_name> [default: xsrf_token] |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
=item xsrf token missing |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
The application has been configured to accept an 'X-' header and no token |
554
|
|
|
|
|
|
|
value was found in either the header or a suitable form field. [default: undef] |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
=item cookie missing |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
There is no cookie with the name specified by C<$self->cookie_name> [default: |
559
|
|
|
|
|
|
|
PSGI-XSRF-Token] |
560
|
|
|
|
|
|
|
|
561
|
|
|
|
|
|
|
=item invalid token |
562
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
The cookie token and form value were both submitted correctly but the values |
564
|
|
|
|
|
|
|
do not match. |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
=item invalid signature |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
The cookies signature is invalid, indicating it was tampered with on the way |
569
|
|
|
|
|
|
|
to the browser. |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
=back |
572
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
=head2 detect_xsrf($self, $request, $env) |
574
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
returns a message explaining the XSRF-related problem, or C<undef> if |
576
|
|
|
|
|
|
|
there's no problem |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
=head2 should_be_filtered($self, $request, $env, $res) |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
returns true if the response should be filtered by this middleware |
581
|
|
|
|
|
|
|
(currently, if its content-type matches C<contents_to_filter_regex>) |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
=head2 generate_token($self, $request, $env, $res) |
584
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
Returns the token value to use for this response. |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
If the cookie is already set, and we do not want a different token for |
588
|
|
|
|
|
|
|
each request, returns the cookie's value. |
589
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
Otherwise, generates a new value based on some random data. If |
591
|
|
|
|
|
|
|
C<secret> is set, the value is also signed. |
592
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
=head2 cookie_handler($self, $request, $env, $res, $token) |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
sets the given token as a cookie in the response |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
=head2 filter_response_html($self, $request, $env, $res, $token) |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
Filters the response, injecting C<< <input> >> elements with the token |
600
|
|
|
|
|
|
|
value into all forms whose method matches C<http_method_regex>. |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
Streaming responses are still streaming after the filtering. |
603
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
=head2 filter_response($self, $request, $env) |
605
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
Calls the application, and (if the response L<< /C<should_be_filtered> |
607
|
|
|
|
|
|
|
>>), it injects the token in the cookie and (if L<< |
608
|
|
|
|
|
|
|
/C<inject_form_input> >>) the forms. |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
=head2 invalid_signature($self, $value) |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
Returns true if the value is not correctly signed. If we're not |
613
|
|
|
|
|
|
|
signing tokens, this method always returns false. |
614
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
=head2 xsrf_detected($self, $args) |
616
|
|
|
|
|
|
|
|
617
|
|
|
|
|
|
|
Invoked when the XSRF is detected. Calls the L<< /C<blocked> >> |
618
|
|
|
|
|
|
|
coderef if we have it, or returns a 403. |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
The C<blocked> coderef is invoked like: |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
$self->blocked->($env,$msg, app => $self->app); |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
=over |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
=item * |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
the original request PSGI environment |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
=item * |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
the error message (from L<< /C<detect_xsrf> >>) |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=item * |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
a hash, currently C<< app => $self->app >>, so you can call the |
637
|
|
|
|
|
|
|
original application |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
=back |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
=head2 log($self, $level, $msg) |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
log through the PSGI logger, if defined |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
=head1 EXPLANATION |
646
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
This module is similar in nature and intention to |
648
|
|
|
|
|
|
|
L<Plack::Middleware::CSRFBlock> but implements the xSRF prevention in a |
649
|
|
|
|
|
|
|
different manner. |
650
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
The solution implemented in this module is based on a CodingHorror article - |
652
|
|
|
|
|
|
|
L<Preventing CSRF and XSRF Attacks|http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html>. |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
The driving comment behind this implementation is from |
655
|
|
|
|
|
|
|
L<the Felten and Zeller paper|https://www.eecs.berkeley.edu/~daw/teaching/cs261-f11/reading/csrf.pdf>: |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
When a user visits a site, the site should generate a (cryptographically |
658
|
|
|
|
|
|
|
strong) pseudorandom value and set it as a cookie on the user's machine. |
659
|
|
|
|
|
|
|
The site should require every form submission to include this pseudorandom |
660
|
|
|
|
|
|
|
value as a form value and also as a cookie value. When a POST request is |
661
|
|
|
|
|
|
|
sent to the site, the request should only be considered valid if the form |
662
|
|
|
|
|
|
|
value and the cookie value are the same. When an attacker submits a form |
663
|
|
|
|
|
|
|
on behalf of a user, he can only modify the values of the form. An |
664
|
|
|
|
|
|
|
attacker cannot read any data sent from the server or modify cookie |
665
|
|
|
|
|
|
|
values, per the same-origin policy. This means that while an attacker can |
666
|
|
|
|
|
|
|
send any value he wants with the form, he will be unable to modify or read |
667
|
|
|
|
|
|
|
the value stored in the cookie. Since the cookie value and the form value |
668
|
|
|
|
|
|
|
must be the same, the attacker will be unable to successfully submit a |
669
|
|
|
|
|
|
|
form unless he is able to guess the pseudorandom value. |
670
|
|
|
|
|
|
|
|
671
|
|
|
|
|
|
|
=head2 What's wrong with Plack::Middleware::CSRFBlock? |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
L<Plack::Middleware::CSRFBlock> is a great module. |
674
|
|
|
|
|
|
|
It does a great job of preventing CSRF behaviour with minimal effort. |
675
|
|
|
|
|
|
|
|
676
|
|
|
|
|
|
|
However when we tried to use it uses the session to store information - which |
677
|
|
|
|
|
|
|
works well most of the time but can cause issues with session timeouts or |
678
|
|
|
|
|
|
|
removal (for any number of valid reasons) combined with logging (back) in to |
679
|
|
|
|
|
|
|
the application in another tab (so as not to interfere with the current |
680
|
|
|
|
|
|
|
screen/tab state). |
681
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
Trying to modify the existing module to provide the extra functionality and |
683
|
|
|
|
|
|
|
behaviour we decided worked better for our use seemed too far reaching to try |
684
|
|
|
|
|
|
|
to force into the existing module. |
685
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
=head2 FURTHER READING |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
=over 4 |
689
|
|
|
|
|
|
|
|
690
|
|
|
|
|
|
|
=item * Preventing CSRF and XSRF Attacks |
691
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
L<http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html> |
693
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
=item * Preventing Cross Site Request Forgery (CSRF) |
695
|
|
|
|
|
|
|
|
696
|
|
|
|
|
|
|
L<https://www.golemtechnologies.com/articles/csrf> |
697
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
=item * Cross-Site Request Forgeries: Exploitation and Prevention [PDF] |
699
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
L<https://www.eecs.berkeley.edu/~daw/teaching/cs261-f11/reading/csrf.pdf> |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
=item * Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet |
703
|
|
|
|
|
|
|
|
704
|
|
|
|
|
|
|
L<https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet> |
705
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
=back |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
=head2 SEE ALSO |
709
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
L<Plack::Middleware::CSRFBlock>, |
711
|
|
|
|
|
|
|
L<Plack::Middleware>, |
712
|
|
|
|
|
|
|
L<Plack> |
713
|
|
|
|
|
|
|
|
714
|
|
|
|
|
|
|
=begin markdown |
715
|
|
|
|
|
|
|
|
716
|
|
|
|
|
|
|
## BUILD STATUS |
717
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
[![Build Status](https://travis-ci.org/chiselwright/plack-middleware-xsrfblock.svg?branch=master)](https://travis-ci.org/chiselwright/plack-middleware-xsrfblock) |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
=end markdown |
721
|
|
|
|
|
|
|
|
722
|
|
|
|
|
|
|
=head1 AUTHOR |
723
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
Chisel <chisel@chizography.net> |
725
|
|
|
|
|
|
|
|
726
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
727
|
|
|
|
|
|
|
|
728
|
|
|
|
|
|
|
This software is copyright (c) 2023 by Chisel Wright. |
729
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
731
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
=head1 CONTRIBUTORS |
734
|
|
|
|
|
|
|
|
735
|
|
|
|
|
|
|
=for stopwords Andrey Khozov Ashley Pond V Chisel Daniel Perrett Gianni Ceccarelli Karen Etheridge Matthew Ryall Matthias Zeichmann Michael Kröll Sebastian Willert Sterling Hanenkamp William Wolf |
736
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
=over 4 |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
=item * |
740
|
|
|
|
|
|
|
|
741
|
|
|
|
|
|
|
Andrey Khozov <andrey@rydlab.ru> |
742
|
|
|
|
|
|
|
|
743
|
|
|
|
|
|
|
=item * |
744
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
Ashley Pond V <ashley.pond.v@gmail.com> |
746
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
=item * |
748
|
|
|
|
|
|
|
|
749
|
|
|
|
|
|
|
Chisel <chisel.wright@net-a-porter.com> |
750
|
|
|
|
|
|
|
|
751
|
|
|
|
|
|
|
=item * |
752
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
Daniel Perrett <dp13@sanger.ac.uk> |
754
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
=item * |
756
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
Gianni Ceccarelli <dakkar@thenautilus.net> |
758
|
|
|
|
|
|
|
|
759
|
|
|
|
|
|
|
=item * |
760
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
Gianni Ceccarelli <gianni.ceccarelli@broadbean.com> |
762
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
=item * |
764
|
|
|
|
|
|
|
|
765
|
|
|
|
|
|
|
Karen Etheridge <ether@cpan.org> |
766
|
|
|
|
|
|
|
|
767
|
|
|
|
|
|
|
=item * |
768
|
|
|
|
|
|
|
|
769
|
|
|
|
|
|
|
Matthew Ryall <matt.ryall@gmail.com> |
770
|
|
|
|
|
|
|
|
771
|
|
|
|
|
|
|
=item * |
772
|
|
|
|
|
|
|
|
773
|
|
|
|
|
|
|
Matthias Zeichmann <matthias.zeichmann@gmail.com> |
774
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
=item * |
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
Michael Kröll <michael.kroell@geizhals.at> |
778
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
=item * |
780
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
Sebastian Willert <willert@gmail.com> |
782
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
=item * |
784
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
Sterling Hanenkamp <sterling@ziprecruiter.com> |
786
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
=item * |
788
|
|
|
|
|
|
|
|
789
|
|
|
|
|
|
|
William Wolf <throughnothing@gmail.com> |
790
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
=back |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
=cut |
794
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
__END__ |
796
|
|
|
|
|
|
|
# vim: ts=8 sts=4 et sw=4 sr sta |