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