File Coverage

blib/lib/Catalyst/Plugin/HashedCookies.pm
Criterion Covered Total %
statement 27 106 25.4
branch 0 32 0.0
condition 0 8 0.0
subroutine 9 15 60.0
pod n/a
total 36 161 22.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::HashedCookies;
2             {
3             $Catalyst::Plugin::HashedCookies::VERSION = '1.131710';
4             }
5              
6 2     2   41833 use strict;
  2         4  
  2         88  
7 2     2   11 use warnings FATAL => 'all';
  2         5  
  2         100  
8              
9 2     2   1749 use MRO::Compat;
  2         25342  
  2         80  
10 2     2   2376 use Symbol;
  2         2484  
  2         224  
11 2     2   2219 use Tie::IxHash;
  2         12343  
  2         56  
12 2     2   2982 use CGI::Simple::Cookie;
  2         14931  
  2         62  
13 2     2   1833 use Digest::HMAC_MD5;
  2         3419  
  2         159  
14 2     2   1602 use Digest::HMAC_SHA1;
  2         22974  
  2         186  
15              
16             {
17             package Catalyst::Request::HashedCookies;
18             {
19             $Catalyst::Request::HashedCookies::VERSION = '1.131710';
20             }
21 2     2   27 use base 'Catalyst::Request';
  2         7  
  2         5169  
22              
23             __PACKAGE__->mk_accessors(qw/validhashedcookies invalidhashedcookies/);
24              
25             # reveal whether a hashed cookie passed its integrity check
26             sub valid_cookie {
27 0     0     my $self = shift;
28 0           my $name = shift;
29              
30 0           return exists $self->validhashedcookies->{$name};
31             }
32              
33             # reveal whether a hashed cookie passed its integrity check
34             sub invalid_cookie {
35 0     0     my $self = shift;
36 0           my $name = shift;
37              
38 0           return exists $self->invalidhashedcookies->{$name};
39             }
40             }
41              
42             sub setup {
43 0     0     my $self = shift;
44              
45             # fix request class - thanks once again to mst
46 0 0         if ($self->request_class eq 'Catalyst::Request') {
47 0           $self->request_class('Catalyst::Request::HashedCookies');
48             }
49             else {
50 0           die 'Please make a Request subclass for your application which '.
51             'isa Catalyst::Request::HashedCookies';
52             }
53              
54 0   0       $self->config->{hashedcookies}->{algorithm} ||= 'SHA1';
55 0 0         ( $self->config->{hashedcookies}->{algorithm} =~ m/^(?:SHA1|MD5)$/ )
56             or die 'Request for unknown digest algorithm to '. __PACKAGE__;
57              
58 0 0         exists $self->config->{hashedcookies}->{required}
59             or $self->config->{hashedcookies}->{required} = 1;
60             # not checked - perl's handling of truth will make junk values 'work'
61              
62 0 0         defined $self->config->{hashedcookies}->{key}
63             or die '"key" is a required configuration parameter to '. __PACKAGE__;
64              
65 0           return $self->next::method(@_);
66             }
67              
68              
69             # remove and check hash in Cookie Values
70             sub prepare_cookies {
71 0     0     my $c = shift;
72 0           $c->next::method(@_);
73 0           $c->request->validhashedcookies( {} );
74 0           $c->request->invalidhashedcookies( {} );
75              
76 0           my $hasher = 'Digest::HMAC_'. $c->config->{hashedcookies}->{algorithm};
77 0           my $hmac = $hasher->new( $c->config->{hashedcookies}->{key} );
78              
79 0           while ( my ( $name, $cgicookie ) = each %{ $c->request->cookies } ) {
  0            
80 0           my @values = @{ [ $cgicookie->value ] };
  0            
81 0           my $digest = '';
82              
83             # restore cookie to original Value set by user
84 0 0         if ( scalar @values % 2 == 0 ) {
85 0           my $t = Tie::IxHash->new(@values);
86 0           my $d = $t->Indices('_hashedcookies_digest');
87 0           my $p = $t->Indices('_hashedcookies_padding');
88              
89 0 0         if ( defined $d ) {
90 0           $digest = $t->Values($d);
91 0           splice( @values, $d * 2, 2 );
92             }
93              
94 0 0         if ( defined $p ) {
95 0           splice( @values, $p * 2, 1 );
96             }
97              
98 0           $cgicookie->value( \@values );
99             }
100              
101 0           my $required = $c->config->{hashedcookies}->{required};
102 0 0 0       if ( not $digest and not $required ) {
103 0 0         $c->log->debug("HashedCookies skipping cookie: $name")
104             if $c->debug;
105 0           $hmac->reset;
106 0           next;
107             }
108              
109             # now, we either have no digest but one is required,
110             # or we have a digest that needs checking
111              
112             # $c->log->debug( "HashedCookies is hashing: ". $cgicookie->as_string );
113 0           $hmac->add( $cgicookie->as_string );
114 0           my $result = $hmac->hexdigest; # WARNING!!! $hmac has now been RESET
115              
116             # $c->log->debug( "HashedCookies retrieved digest: '$digest'" )
117             # if $c->debug;
118             # $c->log->debug( "HashedCookies generated digest: '$result'" )
119             # if $c->debug;
120              
121 0 0         if ( $digest eq $result ) {
122 0 0         $c->log->debug("HashedCookies adding valid cookie: $name")
123             if $c->debug;
124 0           ++$c->request->validhashedcookies->{$name};
125             }
126             else {
127 0 0         $c->log->debug("HashedCookies found INVALID cookie: $name")
128             if $c->debug;
129 0           ++$c->request->invalidhashedcookies->{$name};
130             }
131              
132 0           $hmac->reset;
133             }
134              
135 0           return $c;
136             }
137              
138              
139             # check for illegal parameters in cookie set by App, and raise hell if found
140             sub finalize {
141             # need to hook in here, early in the finalize sequence, because Catalyst has
142             # been written to check $c->error *before* it goes on to call finalize_headers
143             # and hence finalize_cookies.
144 0     0     my $c = shift;
145              
146 0           while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
  0            
147              
148             # see finalize_cookies hook, below, for comments
149 0           my $cgicookie = CGI::Simple::Cookie->new(
150             -name => $name,
151             -value => $cookie->{value},
152             );
153              
154 0 0         if (defined $cgicookie->value) {
155 0           foreach ( @{ [ $cgicookie->value ] } ) {
  0            
156 0 0 0       if (defined and m/^_hashedcookies_/) {
157 0 0         $c->log->debug('HashedCookies setting $c->error, illegal cookie param from App')
158             if $c->debug;
159 0           $c->error('Attempted use of restricted ("_hashedcookies_*") value in cookie');
160              
161             # don't want to have dud cookie sent to client browser
162 0           delete $c->response->cookies->{$cgicookie->name};
163             }
164             }
165             }
166             }
167              
168 0           $c->next::method(@_);
169 0           return $c;
170             }
171              
172              
173             # alter all Cookie Values to include a hash
174             sub finalize_cookies {
175 0     0     my $c = shift;
176              
177 0           my $hasher = 'Digest::HMAC_'. $c->config->{hashedcookies}->{algorithm};
178 0           my $hmac = $hasher->new( $c->config->{hashedcookies}->{key} );
179              
180 0           while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
  0            
181              
182             # creating a tmp CGI::Simple::Cookie is handy for as_string,
183             # and also because we can consistenly use ->value as a list
184             #
185             # only -name and -value are used because this is what CGI::Simple::Cookie->parse()
186             # will pass back from an HTTP header - prepare_cookies needs identical hash
187 0           my $cgicookie = CGI::Simple::Cookie->new(
188             -name => $name,
189             -value => $cookie->{value},
190             );
191              
192             # $c->log->debug( "HashedCookies is hashing: ". $cgicookie->as_string );
193 0           $hmac->add( $cgicookie->as_string );
194              
195             # make sure that cookie ->value can be coerced into a hash upon retrieval
196 0 0         if ( scalar @{ [ $cgicookie->value ] } % 2 == 1 ) {
  0            
197 0           $cookie->{value} = [
198 0           '_hashedcookies_padding' => @{ [ $cgicookie->value ] },
199             '_hashedcookies_digest' => $hmac->hexdigest,
200             ];
201             }
202             else {
203 0           $cookie->{value} = [
204 0           @{ [ $cgicookie->value ] },
205             '_hashedcookies_digest' => $hmac->hexdigest,
206             ];
207             }
208              
209 0           $hmac->reset;
210             }
211              
212 0           $c->next::method(@_);
213 0           return $c;
214             }
215              
216             # ABSTRACT: Tamper-resistant HTTP Cookies
217              
218              
219             1;
220              
221             __END__
222             =pod
223              
224             =head1 NAME
225              
226             Catalyst::Plugin::HashedCookies - Tamper-resistant HTTP Cookies
227              
228             =head1 VERSION
229              
230             version 1.131710
231              
232             =head1 SYNOPSIS
233              
234             use Catalyst qw/HashedCookies/;
235             MyApp->config->{hashedcookies} = {
236             key => $secret_key,
237             algorithm => 'SHA1', # optional
238             required => 1, # optional
239             };
240             MyApp->setup;
241              
242             # later, in another part of MyApp...
243              
244             print "this cookie tastes good!\n"
245             if $c->request->valid_cookie('my_cookie_name');
246              
247             =head1 DESCRIPTION
248              
249             =head2 Overview
250              
251             When HTTP cookies are used to store a user's state or identity it's important
252             that your application is able to distinguish legitimate cookies from those
253             that have been edited or created by a malicious user.
254              
255             This module allows you to determine whether a cookie presented by a client was
256             created in its current state by your own application.
257              
258             =head2 Implementation
259              
260             HashedCookies adds a keyed cryptographic hash to each cookie that your
261             application creates, and checks every client-provided cookie for a valid hash.
262              
263             This is done in a transparent way such that you do not need to change B<any>
264             application code that handles cookies when using this plugin. A cookie that
265             fails to contain a valid hash will still be available to your application
266             through C<< $c->request->cookie() >>.
267              
268             Two additional methods within the Catalyst request object allow you to check
269             the status (in other words, the vailidity) of your cookies.
270              
271             =head1 METHODS
272              
273             =head2 Catalyst Request Object Methods
274              
275             =over 4
276              
277             =item C<< $c->request->valid_cookie($cookie_name) >>
278              
279             If a cookie was successfully authenticated then this method will return True,
280             otherwise it will return False.
281              
282             =item C<< $c->request->invalid_cookie($cookie_name) >>
283              
284             If a cookie failed its authentication, then this method will return True,
285             otherwise it will return False. Please read the L</"CONFIGURATION"> section
286             below to understand what 'failed authentication' really means.
287              
288             =back
289              
290             =head1 CONFIGURATION
291              
292             =over 4
293              
294             =item key
295              
296             MyApp->config->{hashedcookies}->{key} = $secret_key;
297              
298             This parameter is B<required>, and sets the secret key that is used to
299             generate a message authentication hash. Clearly, for a returned cookie to be
300             authenticated the same key must be used both when setting the cookie and
301             retrieving it.
302              
303             =item algorithm
304              
305             MyApp->config->{hashedcookies}->{algorithm} = 'SHA1';
306             # or
307             MyApp->config->{hashedcookies}->{algorithm} = 'MD5';
308              
309             This parameter is optional, and will default to C<SHA1> if not set. It
310             instructs the module to use the given message digest algorithm.
311              
312             =item required
313              
314             MyApp->config->{hashedcookies}->{required} = 0;
315             # or
316             MyApp->config->{hashedcookies}->{required} = 1;
317              
318             This parameter is optional, and will default to C<1> if not set.
319              
320             If a cookie is read from the client but does not contain a HashedCookies hash
321             (i.e. this module was not running when the cookie was set), then this
322             parameter controls whether the cookie is ignored.
323              
324             Setting this parameter to True means that a cookie without a hash is treated
325             as if it did have a hash, and therefore the authentication will fail. Setting
326             this parameter to False means that the cookie will be ignored.
327              
328             When a cookie is ignored, neither C<< $c->request->valid_cookie() >> nor C<<
329             $c->request->invalid_cookie() >> will return True, but you can of course still
330             access the cookie through C<< $c->request->cookie() >>.
331              
332             =back
333              
334             =head1 DIAGNOSTICS
335              
336             =over 4
337              
338             =item 'Request for unknown digest algorithm to ...'
339              
340             You have attempted to configure this module with an unrecognized message
341             digest algorithm. Please see the L</"CONFIGURATION"> section for the valid
342             algorithms.
343              
344             =item '"key" is a required configuration parameter to ...'
345              
346             You have forgotten to set the secret key that is used to generate a message
347             authentication hash. See the L</"SYNOPSIS"> or L</"CONFIGURATION"> section for
348             examples of how to set this parameter.
349              
350             =item 'Attempted use of restricted ("_hashedcookies_*") value in cookie'
351              
352             This module adds values to your cookie, and to avoid clashes with your own
353             values they are named in a special way. If you try to set a cookie with values
354             matching this special name format, your Catalyst Engine's default error
355             handler will be triggered, and the response status code will be set to "500".
356              
357             You cannot trap such errors because they are raised after all the application
358             code has run, but you will see the above error in your log file, and your
359             Application will certainly halt so that Catalyst can display its error page.
360              
361             =item 'Please make a Request subclass for your application which isa Catalyst::Request::HashedCookies'
362              
363             In order to properly hook into Catalyst, you need a Class for the Catalyst
364             Request object which isa C<Catalyst::Request::HashedCookies>. This error is
365             thrown not if you are using C<Catalyst::Request> as the Class (this is
366             detected and worked around), but instead some 3rd party Class.
367              
368             It can happen, apparently, to C<Catalyst::Action::REST> users. Please check
369             the Catalyst wiki for some examples on how to fix your application.
370              
371             =back
372              
373             =head1 DEPENDENCIES
374              
375             Other than the natural dependencies of L<Catalyst> and the contents of the
376             standard Perl distribution, you will need the following:
377              
378             =over 4
379              
380             =item *
381              
382             Digest::HMAC
383              
384             =back
385              
386             =head1 BUGS
387              
388             Please report any bugs or feature requests to
389             C<bug-catalyst-plugin-hashedcookies@rt.cpan.org>, or through the web interface
390             at
391             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Plugin-HashedCookies>.
392             I will be notified, and then you'll automatically be notified of progress on
393             your bug as I make changes.
394              
395             =head1 SEE ALSO
396              
397             L<Catalyst>, L<Digest::HMAC_SHA1>, L<Digest::HMAC_MD5>
398              
399             L<http://www.schneier.com/blog/archives/2005/08/new_cryptanalyt.html>
400              
401             =head1 AUTHOR
402              
403             Oliver Gorwits <oliver@cpan.org>
404              
405             =head1 COPYRIGHT AND LICENSE
406              
407             This software is copyright (c) 2013 by University of Oxford.
408              
409             This is free software; you can redistribute it and/or modify it under
410             the same terms as the Perl 5 programming language system itself.
411              
412             =cut
413