File Coverage

blib/lib/WWW/CSRF.pm
Criterion Covered Total %
statement 50 51 98.0
branch 11 12 91.6
condition 9 14 64.2
subroutine 8 8 100.0
pod 2 2 100.0
total 80 87 91.9


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 is used.
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 is used.
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;