File Coverage

blib/lib/CGI/Application/Plugin/LinkIntegrity.pm
Criterion Covered Total %
statement 128 133 96.2
branch 34 42 80.9
condition 20 27 74.0
subroutine 18 18 100.0
pod 4 4 100.0
total 204 224 91.0


line stmt bran cond sub pod time code
1              
2             package CGI::Application::Plugin::LinkIntegrity;
3              
4 8     8   446869 use warnings;
  8         22  
  8         289  
5 8     8   44 use strict;
  8         17  
  8         622  
6              
7             =head1 NAME
8              
9             CGI::Application::Plugin::LinkIntegrity - Make tamper-resisistent links in CGI::Application
10              
11             =head1 VERSION
12              
13             Version 0.06
14              
15             =cut
16              
17             our $VERSION = '0.06';
18              
19             =head1 SYNOPSIS
20              
21             In your application:
22              
23             use base 'CGI::Application';
24             use CGI::Application::Plugin::LinkIntegrity;
25              
26             sub setup {
27             my $self = shift;
28             $self->link_integrity_config(
29             secret => 'some secret string known only to you and me',
30             );
31             }
32              
33             sub account_info {
34             my $self = shift;
35              
36             my $account_id = get_user_account_id();
37              
38             my $template = $self->load_tmpl('account.html');
39              
40             $template->param(
41             'balance' => $self->link("/account.cgi?rm=balance&acct_id=$account_id");
42             'transfer' => $self->link("/account.cgi?rm=transfer&acct_id=$account_id");
43             'withdrawal' => $self->link("/account.cgi?rm=withdrawl&acct_id=$account_id");
44             );
45             }
46              
47             In your template:
48              
49            

Welcome to The Faceless Banking Corp.

50            

Actions:

51            
">Show Balance
52            
">Make a Transfer
53            
">Get Cash
54              
55              
56             This will send the following HTML to the browser:
57              
58            

Welcome to The Faceless Banking Corp.

59            

Actions:

60            
Show Balance
61            
Make a Transfer
62            
Get Cash
63              
64             The URLs created are now tamper-resistent. If the user changes
65             C from C<73> to C<74>, the C<_checksum> will not match, and the
66             system will treat it as an intrusion attempt.
67              
68             =head2 Calling link and self_link directly from the template
69              
70             If you use C or
71             C, you can pass the C
72             C<$self> object into the template and call C and C directly
73             from the template. In your app:
74              
75             $template->param(
76             'app' => $self,
77             'name' => 'gordon',
78             'email' => 'gordon@example.com',
79             );
80              
81             And in your template you can use
82              
83             # Template::Toolkit syntax
84             ...
85              
86             # HTML::Template::Plugin::Dot syntax
87             ">...
88              
89             # Petal syntax
90            
91             tal:attributes="href app/self_link('name', name, 'email', email)">...
92              
93             Note that in the parameters of the call to << link >>, items enclosed in
94             quotes are treated as literal parameters and barewords are treated as
95             template params. So C<'email'> is the literal string, and C is
96             the template paramter named email (in this case 'gordon@example.com').
97              
98             =head1 DESCRIPTION
99              
100             C lets you create
101             tamper-resistent links within your CGI::Application project. When you
102             create an URL with C, a C<_checksum> is added to the URL:
103              
104             my $link = $self->link("/account.cgi?rm=balance&acct_id=73");
105             print $link; # /account.cgi?rm=balance&acct_id=73&_checksum=1d7c4b82d075785de04fa6b98b572691
106              
107             The checksum is a (cryptographic) hash of the URL, plus a secret string
108             known only to the server.
109              
110             If the user attempts to change part of the URL (e.g. a query string
111             parameter, or the PATH_INFO), then the checksum will not match. The run
112             mode will be changed to C, and the C
113             hook will be called.
114              
115             You can define the C run mode yourself, or you can use
116             the default C run mode built into
117             L.
118              
119             You can disable link checking during development by passing a true value
120             to the C parameter of C<< $self->link_integrity_config >>.
121              
122             =cut
123              
124 8     8   44 use Carp;
  8         29  
  8         821  
125 8     8   42 use File::Spec;
  8         12  
  8         192  
126              
127 8     8   7090 use Digest::HMAC;
  8         10738  
  8         389  
128 8     8   1435 use URI;
  8         11436  
  8         167  
129 8     8   4438 use URI::QueryParam;
  8         4047  
  8         455  
130              
131 8     8   122 use Exporter;
  8         18  
  8         365  
132 8         754 use vars qw(
133             @ISA
134             @EXPORT
135             $Default_Secret
136 8     8   44 );
  8         17  
137              
138              
139             @ISA = qw(Exporter);
140             @EXPORT = qw(link self_link path_link link_integrity_config);
141              
142 8     8   99 use CGI::Application;
  8         16  
  8         14509  
143             if (CGI::Application->can('new_hook')) {
144             CGI::Application->new_hook('invalid_checksum');
145             }
146              
147             =head1 METHODS
148              
149             =head2 link_integrity_config
150              
151             Configure the L. Usually, it
152             makes sense to configure this in the C method of your application's
153             base class:
154              
155             use CGI::Application::Plugin::LinkIntegrity;
156             use base 'CGI::Application';
157             package My::Project;
158              
159             sub setup {
160             my $self = shift;
161              
162             $self->run_modes(['bad_user_no_biscuit']);
163             $self->link_integrity_config(
164             secret => 'some secret string known only to you and me',
165             link_tampered_run_mode => 'bad_user_no_biscuit',
166             digest_module => 'Digest::MD5',
167             disable => 1,
168             );
169             }
170              
171             Or you can pull in this configuration info from a config file. For
172             instance, with using L:
173              
174             use CGI::Application::Plugin::LinkIntegrity;
175             use CGI::Application::Plugin::Config::Context;
176              
177             use base 'CGI::Application';
178             package My::Project;
179              
180             sub setup {
181             my $self = shift;
182              
183             $self->conf->init(
184             file => 'app.conf',
185             driver => 'ConfigGeneral',
186             );
187              
188             my $config = $self->conf->context;
189              
190             $self->link_integrity_config(
191             $config->{'LinkIntegrity'},
192             additional_data => sub {
193             my $self = shift;
194             return $self->session->id;
195             },
196             );
197              
198             my $link_tampered_rm = $config->{'LinkIntegrity'}{'link_tampered_run_mode'} || 'link_tampered';
199              
200             $self->run_modes([$link_tampered_rm]);
201             }
202              
203             Then in your configuration file:
204              
205            
206             secret = some REALLY secret string
207             link_tampered_run_mode = bad_user_no_biscuit
208             hash_algorithm = SHA1
209             disable = 1
210            
211              
212             This strategy allows you to enable and disable link checking on the fly
213             by changing the value of C in the config file.
214              
215             The following configuration parameters are available:
216              
217             =over 4
218              
219             =item secret
220              
221             A string known only to your application. At a commandline, you can
222             generate a secret string with md5:
223              
224             $ perl -MDigest::MD5 -le"print Digest::MD5::md5_hex($$, time, rand(42));"
225              
226             Or you can use Data::UUID:
227              
228             $ perl -MData::UUID -le"$ug = new Data::UUID; $uuid = $ug->create; print $ug->to_string($uuid)"
229              
230             If someone knows your secret string, then they can generate their own
231             checksums on arbitrary data that will always pass the integrity check in
232             your application. That's a Bad Thing, so don't let other people know
233             your secret string, and don't use the default secret string if you can
234             help it.
235              
236             =item additional_data
237              
238             You can pass constant additional data to the checksum generator for every link.
239              
240             $self->link_integrity_config(
241             secret => 'really secret',
242             additional_data => 'some other secret data',
243             }
244              
245              
246             For instance, to stop one user from following a second user's link, you
247             can add a user-specific component to the session, such as the user's
248             session id:
249              
250             $self->link_integrity_config(
251             secret => 'really secret',
252             additional_data => sub {
253             my $self = shift;
254             return $self->session->id;
255             }
256             }
257              
258             You can pass a string instead of a subroutine. But in the case of the
259             user's session, a subroutine is useful so that you get the value of the
260             user's session at the time when the checksum is generated, not at the
261             time when the link integrity system is configured.
262              
263             =item checksum_param
264              
265             The name of the checksum parameter, which is added to the query string
266             and contains the cryptographic checksum of link. By default, this
267             parameter is named C<_checksum>.
268              
269             =item link_tampered_run_mode
270              
271             The run mode to go to when it has been detected that the user has
272             tampered with the link. The default is C.
273              
274             See L<"The link_tampered Run Mode">, below.
275              
276             =item digest_module
277              
278             Which digest module to use to create the checksum. Typically, this will
279             be either C or C. However any module
280             supported by C will work.
281              
282             The default C is C.
283              
284             =item checksum_generator
285              
286             If you want to provide a custom subroutine to make your own checksums,
287             you can define your own subroutine do it via the C param.
288             Here's an example of one that uses Digest::SHA2:
289              
290             $self->link_integrity_config(
291             checksum_generator => sub {
292             my ($url, $secret) = @_;
293             require Digest::SHA2;
294              
295             my $ctx = Digest::SHA2->new();
296             $ctx->add($url . $secret);
297              
298             return $ctx->hexdigest;
299             },
300             );
301              
302             =item disable
303              
304             You can disable link checking entirely by setting C to a true
305             value. This can be useful when you are developing or debugging the
306             application and you want the ability to tweak URL params without getting
307             busted.
308              
309             =back
310              
311             =cut
312              
313             my %Config_Defaults = (
314             secret => undef,
315             checksum_param => '_checksum',
316             link_tampered_run_mode => undef,
317             digest_module => 'Digest::MD5',
318             disable => undef,
319             checksum_generator => undef,
320             additional_data => undef,
321             );
322              
323             sub link_integrity_config {
324 66     66 1 132355 my $self = shift;
325              
326 66         151 my $caller = scalar(caller);
327              
328 66         252 $self->new_hook('invalid_checksum');
329 66         598 $caller->add_callback('prerun', \&_check_link_integrity);
330              
331 66         764 my $args;
332 66 50       189 if (ref $_[0] eq 'HASH') {
333 0         0 $args = $_[0];
334             }
335             else {
336 66         215 $args = { @_ };
337             }
338              
339             # Clear config
340 66         202 undef $self->{__PACKAGE__}{__CONFIG};
341 66         176 my $config = _get_config($self, $args);
342              
343 66   50     170 $config->{'link_tampered_run_mode'} ||= 'link_tampered';
344              
345 66         369 my %run_modes = $self->run_modes;
346 66 100       901 unless ($run_modes{$config->{'link_tampered_run_mode'}}) {
347             $self->run_modes($config->{'link_tampered_run_mode'} => sub {
348 3     3   232 return '

Access Denied

';
349 12         81 });
350             }
351 66         365 %run_modes = $self->run_modes;
352              
353             }
354              
355             sub _get_config {
356 206     206   310 my ($self, $args) = @_;
357              
358 206 100       640 if ($self->{__PACKAGE__}{__CONFIG}) {
359 140         403 return $self->{__PACKAGE__}{__CONFIG};
360             }
361 66         445 my $config = $self->{__PACKAGE__}{__CONFIG} = { %Config_Defaults };
362              
363 66 50       228 if ($args) {
364             # Check that all key names are valid
365 66         199 foreach my $key (keys %$args) {
366 93 50       237 unless (exists $config->{$key}) {
367 0         0 croak "CAP::LinkIntegrity: Bad configuration key: $key\n";
368             }
369 93         237 $config->{$key} = $args->{$key};
370             }
371             # Check that checksum_generator is coderef
372 66 100       217 if (exists $args->{'checksum_generator'}) {
373 1 50       4 unless (ref $args->{'checksum_generator'} eq 'CODE') {
374 0         0 croak "CAP::LinkIntegrity: checksum_generator must be coderef\n";
375             }
376             }
377             }
378 66   100     306 $config->{'link_tampered_run_mode'} ||= 'link_tampered';
379              
380 66 50       225 $config->{'secret'} || croak "CAP::LinkIntegrity - You need to provide a secret string to link_integrity_config";
381              
382 66         154 return $config;
383             }
384              
385             =head2 link
386              
387             Create a link, and add a checksum to it.
388              
389             You can add parameters to the link directly in the URL:
390              
391             my $link = $self->link('/cgi-bin/app.cgi?var=value&var2=value2');
392              
393             Or you can add them as a hash of parameters after the URL:
394              
395             my $link = $self->link(
396             '/cgi-bin/app.cgi',
397             'param1' => 'value',
398             'param2' => 'value2',
399             );
400              
401             =cut
402              
403             sub link {
404 18     18 1 3763 my $self = shift;
405 18         31 my $uri = shift;
406              
407 18         46 my $config = _get_config($self);
408              
409 18         86 $uri = URI->new($uri, 'http');
410              
411 18         26813 my @query_form = $uri->query_form;
412              
413 18         1446 push @query_form, @_;
414              
415 18         42 my $additional_data = $config->{'additional_data'};
416 18 100       69 $additional_data = $additional_data->($self) if ref $additional_data eq 'CODE';
417              
418 18         73 my $checksum = _hmac($self, $uri, $additional_data);
419              
420 17         66 $uri->query_form(@query_form);
421 17         1198 $uri->query_param_append($config->{'checksum_param'} => $checksum);
422              
423 17         2708 return $uri;
424             }
425              
426             sub _hmac {
427 65     65   100 my $self = shift;
428 65         95 my $uri = shift;
429 65         92 my $additional_data = shift;
430              
431 65         142 my $config = _get_config($self);
432              
433 65         144 my $secret = $config->{'secret'};
434              
435 65         86 my $digest;
436 65 100       179 if ($config->{'checksum_generator'}) {
437 1         4 $digest = $config->{'checksum_generator'}->($secret, $uri, $additional_data);
438             }
439             else {
440 64   66     817 my $digest_module = $config->{'digest_module'} || croak "CAP::LinkIntegrity: digest_module not configured";
441 63         6669 eval "require $digest_module";
442 63 50       270 if ($@) {
443 0         0 carp "CAP::LinkIntegrity: Requested digest_module ($digest_module) not installed";
444             }
445              
446 63         327 my $hmac = Digest::HMAC->new($secret, $digest_module);
447              
448             # Add all elements of the URL
449 63   100     1936 $hmac->add($uri->scheme || '');
450 63   100     2009 $hmac->add($uri->authority || '');
451 63   50     1405 $hmac->add($uri->port || '');
452 63   50     2211 $hmac->add($uri->path || '');
453              
454 63         1412 foreach my $key (sort $uri->query_param) {
455 121         11464 $hmac->add('key');
456 121         769 $hmac->add($key);
457 121         711 $hmac->add('values');
458 121         746 $hmac->add($_) for sort $uri->query_param($key);
459             }
460              
461 63   100     5859 $hmac->add($additional_data || '');
462 63         432 $digest = $hmac->hexdigest;
463             }
464 64         1384 return $digest;
465             }
466              
467             =head2 self_link
468              
469             Make a link to the current application, with optional parameters, and
470             add a checksum to the URL.
471              
472             my $link = $self->self_link(
473             'param1' => 'value1',
474             'param2' => 'value2',
475             );
476              
477             C preserves the value of the current application's C.
478             For instance if the current URL is:
479              
480             /cgi-bin/app.cgi/some/path?foo=bar # PATH_INFO is 'some/path'
481              
482             Calling:
483              
484             $self->self_link('bar' => 'baz');
485              
486             Will produce the URL:
487              
488             /cgi-bin/app.cgi/some/path?bar=baz
489              
490             If you want to remove the C value or replace it with a new
491             value, use L.
492              
493             =cut
494              
495             sub self_link {
496 2     2 1 1470 my $self = shift;
497              
498 2         8 my $uri = URI->new($self->query->url(-path_info => 1));
499              
500 2 100       2393 $uri->query_form(@_) if @_;
501              
502 2         79 return $self->link($uri);
503             }
504              
505             =head2 path_link
506              
507             Calling C is the same as calling C, except
508             the current value of C can be replaced.
509              
510             my $link = $self->path_link(
511             '/new/path',
512             'param1' => 'value1',
513             'param2' => 'value2',
514             );
515              
516             For instance if the current URL is:
517              
518             /cgi-bin/app.cgi/some/path?foo=bar # PATH_INFO is 'some/path'
519              
520             Calling:
521              
522             $self->path_link('/new/path');
523              
524             Will produce the URL:
525              
526             /cgi-bin/app.cgi/new/path?foo=bar
527              
528             If you want to remove C entirely, call one of the following:
529              
530             $self->path_link;
531             $self->path_link(undef, 'param1' => 'val1', 'param2 => 'val2' ...);
532             $self->path_link('', 'param1' => 'val1', 'param2 => 'val2' ...);
533              
534             If you want to keep the existing C that was passed to the
535             current application, use L instead.
536              
537             =cut
538              
539             sub path_link {
540 4     4 1 8426 my $self = shift;
541 4         8 my $path_info = shift;
542              
543 4         6 my $uri;
544              
545 4         20 $uri = URI->new($self->query->url);
546 4 100       6430 if ($path_info) {
547              
548             # Convert into an array of path elements
549 1         23 my @path_info = File::Spec->splitdir($path_info);
550              
551             # Remove the first element if it is the empty root element
552 1 50       5 shift @path_info unless $path_info[0];
553              
554 1         13 $uri->path_segments($uri->path_segments, @path_info);
555             }
556              
557 4 100       187 $uri->query_form(@_) if @_;
558              
559 4         362 return $self->link($uri);
560             }
561              
562             sub _check_link_integrity {
563 57     57   119041 my $self = shift;
564              
565 57 50       212 unless ($self->{__PACKAGE__}{__CONFIG}) {
566 0         0 croak "CAP::LinkIntegrity - You need to call link_integrity_config before 'prerun' (e.g. in start or cgiapp_init)\n";
567             }
568              
569 57         136 my $config = _get_config($self);
570              
571              
572 57 100       159 return if $config->{'disable'};
573              
574 52         167 my $uri = URI->new($self->query->url(-path_info => 1));
575              
576 52         128668 my @params;
577              
578             # Entry point #1: if the URL contains no params we let it through
579 52 100       203 return unless $self->query->url_param;
580              
581             # Entry point #2: if the URL contains only a single param named 'keywords'
582             # and this param has no value. This is due to the fact that CGI.pm adds
583             # a blank 'keywords' param when the QUERY_STRING is blank
584              
585 48         16664 my @param = $self->query->url_param;
586 48 100 66     3374 if (@param == 1 and $param[0] eq 'keywords') {
587 4         13 my $keywords = $self->query->param('keywords');
588 4 100 66     119 return if !defined $keywords or $keywords eq '';
589             }
590              
591 47         161 foreach my $name (sort $self->query->url_param) {
592 129         3365 foreach my $val (sort $self->query->url_param($name)) {
593 176         10519 push @params, $name, $val;
594             }
595             }
596              
597 47         238 $uri->query_form(@params);
598              
599 47         4558 my $uri_checksum = $uri->query_param_delete($config->{'checksum_param'});
600 47         7483 my $expected_checksum = _hmac($self, $uri, $config->{'additional_data'});
601              
602 47 100 100     350 if (($uri_checksum || '') ne ($expected_checksum || '')) {
      50        
603 25         113 $self->prerun_mode($config->{'link_tampered_run_mode'});
604 25         345 $self->call_hook('invalid_checksum');
605             }
606             }
607              
608              
609             =head1 RUN MODES
610              
611             =head2 The link_tampered Run Mode
612              
613             Your application is redirected to this run mode when it has been
614             detected that the user has tampered with the link. You can change the
615             name of this run mode by changing the value of the
616             C param to C.
617              
618             L provides a default
619             C run mode, which just displays a page with some stern
620             warning text.
621              
622             You can define your own as follows:
623              
624             sub link_tampered {
625             my $self = shift;
626             my $template = $self->load_template('stern_talking_to');
627             return $template->output;
628             }
629              
630             =head1 HOOKS
631              
632             When a link is followed that doesn't match the checksum, the
633             C hook is called. You can add a callback to this hook
634             to do some cleanup such as deleting the user's session. For instance:
635              
636             sub setup {
637             my $self = shift;
638             $self->add_callback('invalid_checksum' => \&bad_user);
639             }
640              
641             sub bad_user {
642             my $self = shift;
643              
644             # The user has been messing with the URLs, possibly trying to
645             # break into the system. We don't tolerate this behaviour.
646             # So we delete the user's session:
647              
648             $self->session->delete;
649             }
650              
651             =head1 AUTHOR
652              
653             Michael Graham, C<< >>
654              
655             =head1 ACKNOWLEDGEMENTS
656              
657             This module was based on the checksum feature originally built into
658             Richard Dice's L.
659              
660             =head1 BUGS
661              
662             Please report any bugs or feature requests to
663             C, or through the web interface at
664             L. I will be notified, and then you'll automatically
665             be notified of progress on your bug as I make changes.
666              
667             =head1 COPYRIGHT & LICENSE
668              
669             Copyright 2005 Michael Graham, All Rights Reserved.
670              
671             This program is free software; you can redistribute it and/or modify it
672             under the same terms as Perl itself.
673              
674             =cut
675              
676             1; # End of CGI::Application::Plugin::LinkIntegrity