File Coverage

CGI/Application/Plugin/ProtectCSRF.pm
Criterion Covered Total %
statement 91 99 91.9
branch 26 46 56.5
condition 7 15 46.6
subroutine 17 18 94.4
pod 3 5 60.0
total 144 183 78.6


line stmt bran cond sub pod time code
1             package CGI::Application::Plugin::ProtectCSRF;
2              
3             =pod
4              
5             =head1 NAME
6              
7             CGI::Application::Plugin::ProtectCSRF - Plug-in protected from CSRF
8              
9             =head1 VERSION
10              
11             1.01
12              
13             =head1 SYNPSIS
14              
15             use Your::App;
16             use base qw(CGI::Application);
17             use CGI::Application::Plugin::Session; # require!!
18             use CGI::Application::Plugin::ProtectCSRF;
19              
20             sub input_form : PublishCSRFID {
21             my $self = shift;
22             do_something();
23             }
24              
25             sub finish : ProtectCSRF {
26             my $self = shift;
27             $self->clear_csrf_id;
28             do_something();
29             }
30              
31             =head1 DESCRIPTION
32              
33             CGI::Application::Plugin::ProtectCSRF is C::A::P protected from CSRF.
34              
35             When CSRF is detected, Forbidden is returned and processing is interrupted.
36              
37             =cut
38              
39 5     5   230978 use strict;
  5         11  
  5         177  
40 5     5   28 use base qw(Exporter);
  5         10  
  5         443  
41 5     5   37 use Carp;
  5         9  
  5         386  
42 5     5   4887 use HTML::TokeParser;
  5         73603  
  5         301  
43 5     5   4166 use Digest::SHA1 qw(sha1_hex);
  5         4373  
  5         397  
44 5     5   5087 use Attribute::Handlers;
  5         40240  
  5         39  
45              
46             our(
47             @EXPORT,
48             $CSRF_ERROR_MODE,
49             $CSRF_ERROR_STATUS,
50             $CSRF_ERROR_TMPL,
51             $CSRF_ID,
52             $CSRF_ID_LENGTH,
53             $CSRF_POST_ONLY,
54             $VERSION
55             );
56              
57             @EXPORT = qw(
58             clear_csrf_id
59             csrf_id
60             protect_csrf_config
61             );
62              
63             $CSRF_ERROR_MODE = "_csrf_error";
64             $CSRF_ERROR_STATUS = 200;
65             $CSRF_ERROR_TMPL = \qq{
66            
67             CSRF ERROR
68            
69            

CSRF ERROR

70            

This access is illegal. you don't have permission to access on this server.

71            
72            
73             };
74             $CSRF_ID = "_csrf_id";
75             $CSRF_POST_ONLY = 0;
76             $VERSION = 1.01;
77              
78             my(%publish_csrf_id_runmodes, %protect_csrf_runmodes);
79              
80             sub import {
81              
82 4     4   58 my $pkg = caller;
83              
84             # C::A::P::Session method check
85 4 50       64 croak("C::A::P::Session module is not load to your app") if !$pkg->can("session");
86              
87 4         44 $pkg->add_callback("prerun", \&_publish_csrf_id);
88 4         157 $pkg->add_callback("prerun", \&_csrf_forbidden);
89 4         49 $pkg->add_callback("postrun", \&_add_csrf_id);
90              
91 4         382 goto &Exporter::import;
92             }
93              
94             =pod
95              
96             =head1 ACTION
97              
98             =head2 PublishCSRFID
99              
100             PublishCSRFID is action publishes CSRF ticket. CSRF ticket is published when I
101             define it as an attribute of runmode method publishing CSRF ticket, and it is saved in session.
102             If there is form tag in HTML to display after the processing end, as for runmode method to
103             publish, CSRF ticket is set automatically by hidden field
104              
105             # publish CSRF ticket
106             sub input_form : PublishCSRFID {
107             my $self = shift;
108             return <
109            
110            
111            
112            
113            
114             HTML
115             }
116            
117             # display html source
118            
119             <- insert hidden field
120            
121            
122            
123            
124              
125             =head2 ProtectCSRF
126              
127             ProtectCSRF is action to protect from CSRF Attack. If session CSRF ticket does not accord
128             with query CSRF ticket, application consideres it to be CSRF attack and refuse to access it.
129             Carry out the processing that you want to perform after having carried out clear_csrf_id method
130             when access it, and it was admitted.
131              
132             sub finish : ProtectCSRF {
133             my $self = shift;
134             $self->clear_csrf_id; # require! There is not a meaning unless I do it
135             do_something(); # The processing that you want to perform (DB processing etc)
136             }
137              
138             =cut
139              
140             sub CGI::Application::PublishCSRFID : ATTR(BEGIN) {
141 4     4 0 2652 my ($package, $symbol, $referent, $attr, $data, $phase) = @_;
142 4         24 $publish_csrf_id_runmodes{$referent} = 1;
143             #$publish_csrf_id_runmodes{*{$symbol}{NAME}} = 1;
144 5     5   1885 }
  5         11  
  5         27  
145              
146             sub CGI::Application::ProtectCSRF : ATTR(BEGIN) {
147 4     4 0 7536 my ($package, $symbol, $referent, $attr, $data, $phase) = @_;
148 4         18 $protect_csrf_runmodes{$referent} = 1;
149 5     5   2686 }
  5         39  
  5         29  
150              
151             =pod
152              
153             =head1 METHOD
154              
155             =head2 csrf_id
156              
157             Get ticket for protect CSRF
158              
159             Example:
160              
161             sub input_form : PublishCSRFID {
162             my $self = shift;
163              
164             my $csrf_id = $self->csrf_id;
165             do_something();
166             }
167              
168             =cut
169              
170             sub csrf_id {
171              
172 4     4 1 3329 my $self = shift;
173 4         32 return $self->session->param($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id});
174             }
175              
176             =head2 protect_csrf_config
177              
178             Initialize ProtectCSRF
179              
180             Option:
181              
182             csrf_error_status : CSRF error status code (default: 200)
183             csrf_error_mode : CSRF error runmode name (default: _csrf_error)
184             csrf_error_tmpl : CSRF error display html. scalarref or filepath or filehandle (default: $CSRF_ERROR_TMPL - scalarref)
185             csrf_error_tmpl_param : CSRF error display html parameter (for HTML::Template)
186             csrf_id : CSRF ticket name (default: _csrf_id)
187             csrf_post_only : CSRF protect runmode request method check(default:0 1:POST Only)
188              
189             Example:
190              
191             sub cgiapp_init {
192             my $self = shift;
193             $self->tmpl_path("/path/to/template");
194             $self->protect_csrf_config(
195             csrf_error_status => 403, # change forbidden
196             csrf_error_tmpl => "csrf_error.tmpl",
197             csrf_error_tmpl_param => { TITLE => "CSRF ERROR", MESSAGE => "your access is csrf!"},
198             csrf_id => "ticket_id",
199             csrf_post_only => 1
200             );
201             }
202              
203             # csrf_error.tmpl
204             <TMPL_VAR NAME=TITLE ESCAPE=HTML>
205            
206            

CSRF Error

207            
208            
209            
210              
211             =cut
212              
213             sub protect_csrf_config {
214              
215 4     4 1 1512 my($self, %args) = @_;
216 4 50       22 if(ref($self->{__CAP_PROTECT_CSRF_CONFIG}) ne "HASH"){
217 4         17 $self->{__CAP_PROTECT_CSRF_CONFIG} = {};
218             }
219              
220 4 50       28 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_status} = exists $args{csrf_error_status} ? $args{csrf_error_status} : $CSRF_ERROR_STATUS;
221 4 50       21 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_mode} = exists $args{csrf_error_mode} ? $args{csrf_error_mode} : $CSRF_ERROR_MODE;
222 4 50       40 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl} = exists $args{csrf_error_tmpl} ? $args{csrf_error_tmpl} : $CSRF_ERROR_TMPL;
223 4         31 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl_param} = {};
224 4 50       21 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id} = exists $args{csrf_id} ? $args{csrf_id} : $CSRF_ID;
225 4 50       21 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_post_only} = exists $args{csrf_post_only} ? $args{csrf_post_only} : $CSRF_POST_ONLY;
226              
227 4 50 33     42 if(ref($args{csrf_error_tmpl_param}) eq "HASH" && keys %{$args{csrf_error_tmpl_param}}){
  4         26  
228 4         21 $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl_param} = $args{csrf_error_tmpl_param};
229             }
230             }
231              
232             =pod
233              
234             =head2 clear_csrf_id
235              
236             Clear csrfid. It is preferable to make it execute after processing ends.
237              
238             Example :
239              
240             sub cgiapp_init {
241             my $self = shift;
242             $self->protect_csrf_config;
243             }
244              
245             sub input {
246             my $self = shift;
247             do_something(). # input form display..
248             }
249            
250             sub confirm : PublishCSRFID {
251             my $self = shift;
252             do_something(). # publish csrf_id and input check and confirm display..
253             }
254              
255             sub complete : ProtectCSRF {
256             my $self = shift;
257             $self->clear_csrf_id(1); # clear csrf_id for CSRF protect
258             do_something(); # DB insert etc..
259             }
260              
261             =cut
262              
263             sub clear_csrf_id {
264              
265 0     0 1 0 my($self, $fast) = @_;
266 0         0 $self->session->clear($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id});
267 0 0       0 $self->session->flush if $fast;
268             }
269              
270             =pod
271              
272             =head1 CALLBACK
273              
274             =head2 _publish_csrf_id
275              
276             prerun callback
277              
278             =cut
279              
280             sub _publish_csrf_id {
281              
282 4     4   95993 my($self, $rm) = @_;
283 4 100       70 return if !exists $publish_csrf_id_runmodes{$self->can($rm)};
284              
285 1 50       34 if(ref($self->{__CAP_PROTECT_CSRF_CONFIG}) ne "HASH"){
286 0         0 $self->protect_csrf_config;
287             }
288              
289 1         19 my @words = ('A'..'Z', 'a'..'z', 0..9, '/', '.');
290 1         6 my $salt = join "", @words[ map { sprintf( "%d", rand(scalar @words) ) } 1..2 ];
  2         64  
291 1         39 my $csrf_id = sha1_hex($salt . time . $$ . rand(10000));
292 1         13 $self->session->param($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id}, $csrf_id);
293             }
294              
295             =pod
296              
297             =head2 _csrf_forbidden
298              
299             prerun callback
300              
301             =cut
302              
303             sub _csrf_forbidden {
304              
305 4     4   79378 my($self, $rm) = @_;
306 4         12 my $err_flg = 0;
307              
308 4 100       35 return if !exists $protect_csrf_runmodes{$self->can($rm)};
309              
310 3 50       18 if(ref($self->{__CAP_PROTECT_CSRF_CONFIG}) ne "HASH"){
311 0         0 $self->protect_csrf_config;
312             }
313              
314 3 50 33     24 if($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_post_only} && $ENV{REQUEST_METHOD} ne "POST"){
315 0         0 $err_flg = 1;
316             } else {
317              
318 3 50 66     13 if(
      33        
319             !$self->query->param($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id}) ||
320             !$self->csrf_id ||
321             $self->query->param($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id}) ne $self->csrf_id
322             ){
323 1         30 $err_flg = 1;
324             }
325             }
326              
327 1 50       4 if($err_flg){
328              
329             $self->run_modes( $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_mode} => sub {
330            
331 1     1   80 my $self = shift;
332 1         14 $self->header_props( -type => "text/html", -status => $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_status} );
333              
334 1         63 my $tmpl_obj = $self->load_tmpl($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl}, die_on_bad_params => 0);
335 1 50       20641 if(keys %{$self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl_param}}){
  1         9  
336 1         3 $tmpl_obj->param(%{$self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_tmpl_param}});
  1         8  
337             }
338 1         63 return $tmpl_obj->output;
339 1         12 });
340 1         33 $self->prerun_mode($self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_error_mode});
341             }
342              
343 1         15 return 0;
344             }
345              
346              
347             =pod
348              
349             =head2 _add_csrf_id
350              
351             postrun callback
352              
353             =cut
354              
355             sub _add_csrf_id {
356              
357 2     2   296 my($self, $scalarref) = @_;
358 2         25 my $rm = $self->get_current_runmode;
359 2         26 my $coderef = $self->can($rm);
360 2 100 66     19 return if !$coderef || !exists $publish_csrf_id_runmodes{$coderef};
361              
362 1 50       7 if(ref($self->{__CAP_PROTECT_CSRF_CONFIG}) ne "HASH"){
363 0         0 $self->protect_csrf_config;
364             }
365              
366             # my %header = $self->header_props;
367             # return if %header && $header{-type} ne "text/html";
368              
369 1         3 my $body = "";
370 1         12 my $hidden = sprintf qq{}, $self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id}, $self->csrf_id;
371              
372 1         36 my $parser = HTML::TokeParser->new($scalarref);
373 1         316 while(my $token = $parser->get_token){
374              
375             # start tag(
sniping)
376 4 100       129 if($token->[0] eq "S"){
    50          
    0          
377            
378 3 100       11 if(lc($token->[1]) eq "form"){
379 1         12 $body .= $token->[4] . "\n" . $hidden;
380             # In the future...
381             #}elsif(lc($token->[1]) eq "a"){
382             #
383             # if(exists $token->[2]->{href} && defined $token->[2]->{href}){
384             # my $uri = URI->new($token->[2]->{href});
385             # my %query_form = $uri->query_form;
386             # $query_form{$self->{__CAP_PROTECT_CSRF_CONFIG}->{csrf_id}} = $self->csrf_id;
387             # $uri->query_form(%query_form);
388             # $token->[2]->{href} = $uri->path_query;
389             # my $prop = join " ", (map { $_ . "=\"" . $token->[2]->{$_} . "\"" } keys %{$token->[2]});
390             # $body .= "<" . lc($token->[1]) . " ". $prop . ">";
391             # }else{
392             # $body .= $token->[4];
393             # }
394              
395             }else{
396 2         13 $body .= $token->[4];
397             }
398              
399             # end tag, process instructions
400             }elsif($token->[0] =~ /^(E|PI)$/){
401 1         6 $body .= $token->[2];
402            
403             # text, comment, declaration
404             }elsif($token->[0] =~ /^(T|C|D)$/){
405 0         0 $body .= $token->[1];
406             }
407             }
408              
409 1         10 ${$scalarref} = $body;
  1         21  
410             }
411              
412             1;
413              
414             __END__