File Coverage

lib/Mojolicious/Plugin/FormTamperingProtector.pm
Criterion Covered Total %
statement 66 66 100.0
branch 20 24 83.3
condition 17 18 94.4
subroutine 16 16 100.0
pod 2 6 33.3
total 121 130 93.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::FormTamperingProtector;
2 3     3   19423 use strict;
  3         4  
  3         90  
3 3     3   12 use warnings;
  3         3  
  3         96  
4 3     3   525 use Mojo::Base 'Mojolicious::Plugin';
  3         7919  
  3         22  
5             our $VERSION = '0.03';
6 3     3   2622 use Data::Dumper;
  3         7996  
  3         210  
7 3     3   478 use Mojo::JSON qw(decode_json encode_json);
  3         65226  
  3         231  
8 3         273 use Mojo::Util qw{encode decode xml_escape hmac_sha1_sum secure_compare
9 3     3   21 b64_decode b64_encode};
  3         4  
10 3     3   1663 use HTML::ValidationRules::Legacy qw{validate extract};
  3         8  
  3         3668  
11              
12             our $TERM_ACTION = 0;
13             our $TERM_SCHEMA = 1;
14              
15             ### ---
16             ### register
17             ### ---
18             sub register {
19 1     1 1 53 my ($self, $app, $opt) = @_;
20            
21 1         3 my $schema_key = $opt->{namespace}. "-schema";
22 1         2 my $sess_key = $opt->{namespace}. '-sessid';
23            
24 1 50       4 my $actions = ref $opt->{action} ? $opt->{action} : [$opt->{action}];
25            
26             $app->hook(before_dispatch => sub {
27 53     53   370074 my $c = shift;
28 53         217 my $req = $c->req;
29            
30 53 100 100     633 if ($req->method eq 'POST' && grep {$_ eq $req->url->path} @$actions) {
  102         2598  
31            
32 50   100     1659 my $wrapper = deserialize(unsign(
33             $req->param($schema_key),
34             ($c->session($sess_key) || ''). $app->secrets->[0]
35             ));
36            
37 50         12647 $req->params->remove($schema_key);
38            
39 50 100       1402 if (!$wrapper) {
40 5         22 return $opt->{blackhole}->($c,
41             'Form schema is missing, possible hacking attempt');
42             }
43 45 100       155 if ($req->url->path ne $wrapper->{$TERM_ACTION}) {
44 1         39 return $opt->{blackhole}->($c,
45             'Action attribute has been tampered');
46             }
47            
48 44 100       1812 if (my $err = validate($wrapper->{$TERM_SCHEMA}, $req->params)) {
49 23         128 return $opt->{blackhole}->($c, $err);
50             }
51             }
52 1         10 });
53            
54             $app->hook(after_dispatch => sub {
55 53     53   49141 my $c = shift;
56            
57 53 100 100     134 if ($c->res->headers->content_type =~ qr{^text/html} &&
58             $c->res->body =~ qr{
59            
60 1         76 my $sessid = $c->session($sess_key);
61            
62 1 50       201 if (! $sessid) {
63 1         35 $sessid = hmac_sha1_sum(time(). {}. rand(), $$);
64 1         5 $c->session($sess_key => $sessid);
65             }
66            
67 1         58 $c->res->body(inject(
68             $c->res->body,
69             $actions,
70             $schema_key,
71             $sessid. $app->secrets->[0],
72             $c->res->content->charset)
73             );
74             }
75 1         23 });
76             }
77              
78             sub inject {
79 1     1 1 71 my ($html, $actions, $token_key, $secret, $charset) = @_;
80            
81 1 50       4 if (! ref $html) {
82 1 50       6 $html = Mojo::DOM->new($charset ? decode($charset, $html) : $html);
83             }
84              
85             $html->find(qq{form[action][method="post"]})->each(sub {
86 18     18   7961 my $form = shift;
87 18         42 my $action = $form->attr('action');
88            
89 18 100       252 return if (! grep {$_ eq $action} @$actions);
  36         93  
90            
91 17         54 my $wrapper = sign(serialize({
92             $TERM_ACTION => $action,
93             $TERM_SCHEMA => extract($form, $charset),
94             }), $secret);
95            
96 17         91 $form->append_content(sprintf(<<"EOF", $token_key, xml_escape $wrapper));
97            
98            
99            
100             EOF
101 1         10946 });
102            
103 1         301 return encode($charset, $html);
104             }
105              
106             sub serialize {
107 37   100 37 0 10545 return b64_encode(encode_json(shift // return), '');
108             }
109              
110             sub deserialize {
111 80   100 80 0 3563 return decode_json(b64_decode(shift // return));
112             }
113              
114             sub sign {
115 21     21 0 2391 my ($value, $secret) = @_;
116 21         235 return $value. '--' . hmac_sha1_sum($value, $secret);
117             }
118              
119             sub unsign {
120 72     72 0 171385 my ($value, $secret) = @_;
121 72 100 66     1155 if ($value && $secret && $value =~ s/--([^\-]+)$//) {
      100        
122 68         182 my $sig = $1;
123 68 100       836 return $value if (secure_compare($sig, hmac_sha1_sum($value, $secret)));
124             }
125 8         180 return;
126             }
127              
128             1;
129              
130             __END__