| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package WWW::CSRF; |
|
2
|
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
=pod |
|
4
|
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 NAME |
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
WWW::CSRF - Generate and check tokens to protect against CSRF attacks |
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
use WWW::CSRF qw(generate_csrf_token check_csrf_token CSRF_OK); |
|
12
|
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
Generate a token to add as a hidden in all HTML forms: |
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
my $csrf_token = generate_csrf_token($username, "s3kr1t"); |
|
16
|
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
Then, in any action with side effects, retrieve that form field |
|
18
|
|
|
|
|
|
|
and check it with: |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
my $status = check_csrf_token($username, "s3kr1t", $csrf_token); |
|
21
|
|
|
|
|
|
|
die "Wrong CSRF token" unless ($status == CSRF_OK); |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
=head1 COPYRIGHT |
|
24
|
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
Copyright 2013 Steinar H. Gunderson. |
|
26
|
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
This library is free software; you can redistribute it and/or |
|
28
|
|
|
|
|
|
|
modify it under the same terms as Perl itself. |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
This module generates tokens to help protect against a website |
|
33
|
|
|
|
|
|
|
attack known as Cross-Site Request Forgery (CSRF, also known |
|
34
|
|
|
|
|
|
|
as XSRF). CSRF is an attack where an attacker fools a browser into |
|
35
|
|
|
|
|
|
|
make a request to a web server for which that browser will |
|
36
|
|
|
|
|
|
|
automatically include some form of credentials (cookies, cached |
|
37
|
|
|
|
|
|
|
HTTP Basic authentication, etc.), thus abusing the web server's |
|
38
|
|
|
|
|
|
|
trust in the user for malicious use. |
|
39
|
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
The most common CSRF mitigation is sending a special, hard-to-guess |
|
41
|
|
|
|
|
|
|
token with every request, and then require that any request that |
|
42
|
|
|
|
|
|
|
is not idempotent (i.e., has side effects) must be accompanied |
|
43
|
|
|
|
|
|
|
with such a token. This mitigation depends critically on the fact |
|
44
|
|
|
|
|
|
|
that while an attacker can easily make the victim's browser |
|
45
|
|
|
|
|
|
|
I a request, the browser security model (same-origin policy, |
|
46
|
|
|
|
|
|
|
or SOP for short) prevents third-party sites from reading the |
|
47
|
|
|
|
|
|
|
I of that request. |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
CSRF tokens should have at least the following properties: |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
=over |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
=item * |
|
54
|
|
|
|
|
|
|
They should be hard-to-guess, so they should be signed |
|
55
|
|
|
|
|
|
|
with some key known only to the server. |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
=item * |
|
58
|
|
|
|
|
|
|
They should be dependent on the authenticated identity, |
|
59
|
|
|
|
|
|
|
so that one user cannot use its own tokens to impersonate |
|
60
|
|
|
|
|
|
|
another user. |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
=item * |
|
63
|
|
|
|
|
|
|
They should not be the same for every request, or an |
|
64
|
|
|
|
|
|
|
attack known as BREACH can use HTTP compression |
|
65
|
|
|
|
|
|
|
to gradually deduce more and more of the token. |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=item * |
|
68
|
|
|
|
|
|
|
They should contain an (authenticated) timestamp, so |
|
69
|
|
|
|
|
|
|
that if an attacker manages to learn one token, he or she |
|
70
|
|
|
|
|
|
|
cannot impersonate a user indefinitely. |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
=back |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
WWW::CSRF simplifies the (simple, but tedious) work of creating and verifying |
|
75
|
|
|
|
|
|
|
such tokens. |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
Note that resources that are protected against CSRF should also be protected |
|
78
|
|
|
|
|
|
|
against a different attack known as clickjacking. There are many defenses |
|
79
|
|
|
|
|
|
|
against clickjacking (which ideally should be combined), but a good start is |
|
80
|
|
|
|
|
|
|
sending a C HTTP header set to C or C. |
|
81
|
|
|
|
|
|
|
See the L |
|
82
|
|
|
|
|
|
|
for more information. |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
This module provides the following functions: |
|
85
|
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
=over 4 |
|
87
|
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
=cut |
|
89
|
|
|
|
|
|
|
|
|
90
|
3
|
|
|
3
|
|
54515
|
use strict; |
|
|
3
|
|
|
|
|
7
|
|
|
|
3
|
|
|
|
|
112
|
|
|
91
|
3
|
|
|
3
|
|
14
|
use warnings; |
|
|
3
|
|
|
|
|
6
|
|
|
|
3
|
|
|
|
|
88
|
|
|
92
|
3
|
|
|
3
|
|
3279
|
use Bytes::Random::Secure; |
|
|
3
|
|
|
|
|
45439
|
|
|
|
3
|
|
|
|
|
404
|
|
|
93
|
3
|
|
|
3
|
|
2652
|
use Digest::HMAC_SHA1; |
|
|
3
|
|
|
|
|
35247
|
|
|
|
3
|
|
|
|
|
159
|
|
|
94
|
|
|
|
|
|
|
use constant { |
|
95
|
3
|
|
|
|
|
2376
|
CSRF_OK => 0, |
|
96
|
|
|
|
|
|
|
CSRF_EXPIRED => 1, |
|
97
|
|
|
|
|
|
|
CSRF_INVALID_SIGNATURE => 2, |
|
98
|
|
|
|
|
|
|
CSRF_MALFORMED_TOKEN => 3, |
|
99
|
3
|
|
|
3
|
|
27
|
}; |
|
|
3
|
|
|
|
|
7
|
|
|
100
|
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
require Exporter; |
|
102
|
|
|
|
|
|
|
our @ISA = qw(Exporter); |
|
103
|
|
|
|
|
|
|
our @EXPORT_OK = qw(generate_csrf_token check_csrf_token CSRF_OK CSRF_MALFORMED_TOKEN CSRF_INVALID_SIGNATURE CSRF_EXPIRED); |
|
104
|
|
|
|
|
|
|
our $VERSION = '1.00'; |
|
105
|
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
=item generate_csrf_token($id, $secret, \%options) |
|
107
|
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
This routine generates a CSRF token to send out to already authenticated users. |
|
109
|
|
|
|
|
|
|
(Unauthenticated users generally need no CSRF protection, as there are no |
|
110
|
|
|
|
|
|
|
credentials to impersonate.) |
|
111
|
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
$id is the identity you wish to authenticate; usually, this would be a user name |
|
113
|
|
|
|
|
|
|
of some sort. |
|
114
|
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
$secret is the secret key authenticating the token. This should be protected in |
|
116
|
|
|
|
|
|
|
the same matter you would protect other server-side secrets, e.g. database |
|
117
|
|
|
|
|
|
|
passwords--if this leaks out, an attacker can generate CSRF tokens at will. |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
The keys in %options are relatively esoteric and need generally not be set, |
|
120
|
|
|
|
|
|
|
but currently supported are: |
|
121
|
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
=over |
|
123
|
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
=item * |
|
125
|
|
|
|
|
|
|
C |
|
126
|
|
|
|
|
|
|
set, the value of C |
|
127
|
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
=item * |
|
129
|
|
|
|
|
|
|
C, for controlling the random masking value used to protect against |
|
130
|
|
|
|
|
|
|
the BREACH attack. If set, it must be exactly 20 random bytes; if not, |
|
131
|
|
|
|
|
|
|
these bytes are generated with a call to L. |
|
132
|
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
=back |
|
134
|
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
The returned CSRF token is in a text-only form suitable for inserting into |
|
136
|
|
|
|
|
|
|
a HTML form without further escaping (assuming you did not send in strange |
|
137
|
|
|
|
|
|
|
things to the C |
|
138
|
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
=cut |
|
140
|
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
sub generate_csrf_token { |
|
142
|
6
|
|
|
6
|
1
|
24
|
my ($id, $secret, $options) = @_; |
|
143
|
|
|
|
|
|
|
|
|
144
|
6
|
|
66
|
|
|
30
|
my $time = $options->{'Time'} // time; |
|
145
|
6
|
|
|
|
|
9
|
my $random = $options->{'Random'}; |
|
146
|
|
|
|
|
|
|
|
|
147
|
6
|
|
|
|
|
27
|
my $digest = Digest::HMAC_SHA1::hmac_sha1($time . "/" . $id, $secret); |
|
148
|
6
|
|
|
|
|
163
|
my @digest_bytes = _to_byte_array($digest); |
|
149
|
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
# Mask the token to avoid the BREACH attack. |
|
151
|
6
|
100
|
|
|
|
28
|
if (!defined($random)) { |
|
|
|
100
|
|
|
|
|
|
|
152
|
1
|
|
|
|
|
6
|
$random = Bytes::Random::Secure::random_bytes(scalar @digest_bytes); |
|
153
|
|
|
|
|
|
|
} elsif (length($random) != length($digest)) { |
|
154
|
1
|
|
|
|
|
14
|
die "Given randomness is of the wrong length (should be " . length($digest) . " bytes)"; |
|
155
|
|
|
|
|
|
|
} |
|
156
|
5
|
|
|
|
|
601
|
my @random_bytes = _to_byte_array($random); |
|
157
|
|
|
|
|
|
|
|
|
158
|
5
|
|
|
|
|
7
|
my $masked_token = ""; |
|
159
|
5
|
|
|
|
|
7
|
my $mask = ""; |
|
160
|
5
|
|
|
|
|
14
|
for my $i (0..$#digest_bytes) { |
|
161
|
100
|
|
|
|
|
155
|
$masked_token .= sprintf "%02x", ($digest_bytes[$i] ^ $random_bytes[$i]); |
|
162
|
100
|
|
|
|
|
145
|
$mask .= sprintf "%02x", $random_bytes[$i]; |
|
163
|
|
|
|
|
|
|
} |
|
164
|
|
|
|
|
|
|
|
|
165
|
5
|
|
|
|
|
51
|
return sprintf("%s,%s,%d", $masked_token, $mask, $time); |
|
166
|
|
|
|
|
|
|
} |
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
=item check_csrf_token($id, $secret, $csrf_token, \%options) |
|
169
|
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
This routine checks the integrity and age of the a token generated by |
|
171
|
|
|
|
|
|
|
C. The values of $id and $secret correspond to |
|
172
|
|
|
|
|
|
|
the same parameters given to C, and $csrf_token |
|
173
|
|
|
|
|
|
|
is the token to verify. Also, you can set one or more of the following |
|
174
|
|
|
|
|
|
|
options in %options: |
|
175
|
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
=over |
|
177
|
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=item * |
|
179
|
|
|
|
|
|
|
C |
|
180
|
|
|
|
|
|
|
token. If this is not set, the value of C |
|
181
|
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
=item * |
|
183
|
|
|
|
|
|
|
C, for setting a maximum age for the CSRF token in seconds. |
|
184
|
|
|
|
|
|
|
If this is negative, I, which is not |
|
185
|
|
|
|
|
|
|
recommended. The default value is a week, or 604800 seconds. |
|
186
|
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
=back |
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
This routine returns one of the following constants: |
|
190
|
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
=over |
|
192
|
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=item * |
|
194
|
|
|
|
|
|
|
C: The token is verified correct. |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=item * |
|
197
|
|
|
|
|
|
|
C: The token has an expired timestamp, but is otherwise |
|
198
|
|
|
|
|
|
|
valid. |
|
199
|
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
=item * |
|
201
|
|
|
|
|
|
|
C: The token is not properly authenticated; |
|
202
|
|
|
|
|
|
|
either it was generated using the wrong secret, for the wrong user, |
|
203
|
|
|
|
|
|
|
or it has been tampered with in-transit. |
|
204
|
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=item * |
|
206
|
|
|
|
|
|
|
C: The token is not in the correct format. |
|
207
|
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=back |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
In general, you should only allow the requested action if C |
|
211
|
|
|
|
|
|
|
returns C. |
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
Note that you are allowed to call C multiple times with |
|
214
|
|
|
|
|
|
|
e.g. different secrets. This is useful in the case of key rollover, where |
|
215
|
|
|
|
|
|
|
you change the secret for new tokens, but want to continue accepting old |
|
216
|
|
|
|
|
|
|
tokens for some time to avoid disrupting operations. |
|
217
|
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
=cut |
|
219
|
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
sub check_csrf_token { |
|
221
|
7
|
|
|
7
|
1
|
28
|
my ($id, $secret, $csrf_token, $options) = @_; |
|
222
|
|
|
|
|
|
|
|
|
223
|
7
|
100
|
|
|
|
47
|
if ($csrf_token !~ /^([0-9a-f]+),([0-9a-f]+),([0-9]+)$/) { |
|
224
|
1
|
|
|
|
|
6
|
return CSRF_MALFORMED_TOKEN; |
|
225
|
|
|
|
|
|
|
} |
|
226
|
|
|
|
|
|
|
|
|
227
|
6
|
|
66
|
|
|
32
|
my $ref_time = $options->{'Time'} // time; |
|
228
|
|
|
|
|
|
|
|
|
229
|
6
|
|
|
|
|
22
|
my ($masked_token, $mask, $time) = ($1, $2, $3); |
|
230
|
6
|
|
50
|
|
|
19
|
my $max_age = $options->{'MaxAge'} // (86400*7); |
|
231
|
|
|
|
|
|
|
|
|
232
|
6
|
|
|
|
|
33
|
my @masked_bytes = _to_byte_array(pack('H*', $masked_token)); |
|
233
|
6
|
|
|
|
|
25
|
my @mask_bytes = _to_byte_array(pack('H*', $mask)); |
|
234
|
|
|
|
|
|
|
|
|
235
|
6
|
|
|
|
|
35
|
my $correct_token = Digest::HMAC_SHA1::hmac_sha1($time . '/' . $id, $secret); |
|
236
|
6
|
|
|
|
|
159
|
my @correct_bytes = _to_byte_array($correct_token); |
|
237
|
|
|
|
|
|
|
|
|
238
|
6
|
50
|
33
|
|
|
39
|
if ($#masked_bytes != $#mask_bytes || $#masked_bytes != $#correct_bytes) { |
|
239
|
|
|
|
|
|
|
# Malformed token (wrong number of characters). |
|
240
|
0
|
|
|
|
|
0
|
return CSRF_MALFORMED_TOKEN; |
|
241
|
|
|
|
|
|
|
} |
|
242
|
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
# Compare in a way that should make timing attacks hard. |
|
244
|
6
|
|
|
|
|
9
|
my $mismatches = 0; |
|
245
|
6
|
|
|
|
|
16
|
for my $i (0..$#masked_bytes) { |
|
246
|
120
|
|
|
|
|
169
|
$mismatches += $masked_bytes[$i] ^ $mask_bytes[$i] ^ $correct_bytes[$i]; |
|
247
|
|
|
|
|
|
|
} |
|
248
|
6
|
100
|
|
|
|
16
|
if ($mismatches == 0) { |
|
249
|
3
|
100
|
100
|
|
|
17
|
if ($max_age >= 0 && $ref_time - $time > $max_age) { |
|
250
|
1
|
|
|
|
|
7
|
return CSRF_EXPIRED; |
|
251
|
|
|
|
|
|
|
} else { |
|
252
|
2
|
|
|
|
|
19
|
return CSRF_OK; |
|
253
|
|
|
|
|
|
|
} |
|
254
|
|
|
|
|
|
|
} else { |
|
255
|
3
|
|
|
|
|
21
|
return CSRF_INVALID_SIGNATURE; |
|
256
|
|
|
|
|
|
|
} |
|
257
|
|
|
|
|
|
|
} |
|
258
|
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
# Converts each byte in the given string to its numeric value, |
|
260
|
|
|
|
|
|
|
# e.g., "ABCabc" becomes (65, 66, 67, 97, 98, 99). |
|
261
|
|
|
|
|
|
|
sub _to_byte_array { |
|
262
|
29
|
|
|
29
|
|
163
|
return unpack("C*", $_[0]); |
|
263
|
|
|
|
|
|
|
} |
|
264
|
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
=back |
|
266
|
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
=head1 SEE ALSO |
|
268
|
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
Wikipedia has an article with more information on CSRF: |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
L |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
=cut |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
1; |