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