File Coverage

blib/lib/App/HTTP_Proxy_IMP/IMP/CSRFprotect.pm
Criterion Covered Total %
statement 26 113 23.0
branch 3 46 6.5
condition 0 3 0.0
subroutine 8 17 47.0
pod n/a
total 37 179 20.6


line stmt bran cond sub pod time code
1             # PoC for CSRF protection
2             # see pod at the end for detailed description of the idea and references
3              
4 1     1   991 use strict;
  1         2  
  1         34  
5 1     1   5 use warnings;
  1         2  
  1         36  
6             package App::HTTP_Proxy_IMP::IMP::CSRFprotect;
7 1     1   5 use base 'Net::IMP::HTTP::Request';
  1         2  
  1         129  
8             use fields (
9 1         5 'target', # target domain from request header
10             'origin', # domain from origin/referer request header
11 1     1   7 );
  1         3  
12              
13 1     1   63 use Net::IMP qw(:DEFAULT :log);
  1         12  
  1         181  
14 1     1   7 use Net::IMP::Debug;
  1         3  
  1         6  
15 1     1   95 use Net::IMP::HTTP;
  1         3  
  1         1770  
16              
17             sub RTYPES { return (
18 0     0     IMP_REPLACE, # remove Cookie/Authorization header
19             IMP_LOG, # log if we removed something
20             IMP_DENY, # bad requests/responses
21             IMP_PASS,
22             )}
23              
24             sub new_analyzer {
25 0     0     my ($class,%args) = @_;
26 0           my $self = $class->SUPER::new_analyzer(%args);
27 0           $self->run_callback(
28             # we will not modify response, but need to look at the response
29             # header to detect redirects. After the response header was seen
30             # this will be upgraded to IMP_PASS
31             [ IMP_PREPASS,1,IMP_MAXOFFSET]
32             );
33 0           return $self;
34             }
35              
36             sub request_hdr {
37 0     0     my ($self,$hdr) = @_;
38             # modify if necessary, rest of request can be forwarded w/o inspection
39 0           my $len = length($hdr);
40 0           my @rv;
41 0 0         if ( defined( my $newhdr = _modify_rqhdr($self,$hdr))) {
42 0           push @rv, [ IMP_REPLACE,0,$len,$newhdr ];
43             }
44 0           $self->run_callback(@rv,[ IMP_PASS,0,IMP_MAXOFFSET ]);
45             }
46              
47             sub response_hdr {
48 0     0     my ($self,$hdr) = @_;
49             # response header
50 0           _analyze_rphdr($self,$hdr);
51 0           $self->run_callback([ IMP_PASS,1,IMP_MAXOFFSET ]); # upgrade to IMP_PASS
52             }
53              
54              
55             {
56             # FIXME - should expire after a short time
57             # FIXME - for multi-process environments it needs to be shared
58             # between processes
59             # DELEGATION{FROM}{TO}: e.g. FROM delegated to TO by
60             # - having a POST request to TO with origin/referer FROM
61             # - having a redirect to TO in response from FROM
62             my %DELEGATION;
63              
64             sub _delegate {
65 0     0     my ($origin,$target,$why) = @_;
66 0 0         if ( $DELEGATION{$origin}{$target} ) {
67 0           debug("refresh delegation $origin -> $target ($why)");
68 0           $DELEGATION{$origin}{$target} = 1;
69             } else {
70 0           debug("add delegation $origin -> $target ($why)");
71 0           $DELEGATION{$origin}{$target} = 1;
72             }
73             }
74              
75             sub _delegation_exists {
76 0     0     my ($origin,$target) = @_;
77 0           return $DELEGATION{$origin}{$target};
78             }
79             }
80              
81             # extract target and origin domain
82             # if they differ remove cookies and authorization infos unless we have
83             # an established trust between these domains
84             my $rx_host = qr{([\w\-.]+|\[[\da-fA-F:.]+\])};
85             my $rx_host_from_url = qr{^https?://$rx_host};
86             sub _modify_rqhdr {
87 0     0     my ($self,$hdr) = @_;
88            
89             # determine target
90 0           my (@target) = $hdr =~m{\A\w+[ \t]+http://$rx_host};
91 0 0         @target = _gethdr($hdr,'Host',$rx_host) if ! @target;
92 0 0 0       if ( ! @target or @target>1 ) {
93 0           $self->run_callback(
94             [ IMP_LOG,0,0,0,IMP_LOG_WARNING,
95             "cannot determine target from request\n".$hdr ],
96             [ IMP_DENY,0, "cannot determine target from request" ]
97             );
98 0           return;
99             }
100              
101             # determine referer/origin domain
102 0           my @origin = _gethdr($hdr,'Origin',$rx_host_from_url);
103 0 0         @origin = _gethdr($hdr,'Referer',$rx_host_from_url) if ! @origin;
104 0 0         if ( @origin > 1 ) {
105             # invalid: conflicting origins
106 0           $self->run_callback(
107             [ IMP_LOG,0,0,0,IMP_LOG_WARNING,
108             "conflicting origins in request\n".$hdr ],
109             [ IMP_DENY,0, "conflicting origins in request" ]
110             );
111 0           return;
112             }
113              
114 0 0         if ( ! @origin ) {
115             # we have no origin to check trust inside request
116 0           debug("no origin to check trust in request to @target");
117              
118             } else {
119             # do nothing unless the request is cross-origin
120 0           $self->{origin} = $origin[0];
121 0           $self->{target} = $target[0];
122 0 0         return if $origin[0] eq $target[0];
123              
124             # implicite trust when both have the same root-domain
125 0           my $origin = _rootdom($origin[0]);
126 0           my $target = _rootdom($target[0]);
127 0 0         if ( $origin eq $target ) {
128 0           debug("trusted request from $origin[0] to $target[0] (same root-dom)");
129             return
130 0           }
131              
132             # check if this is a delegation (POST)
133 0 0         if ( $hdr =~m{\APOST } ) {
134 0           _delegate($origin,$target,'POST');
135             }
136              
137             # consider the request trused if we got an earlier delegation from
138             # $target to $origin (e.g. in the other direction)
139 0 0         if ( _delegation_exists($target,$origin)) {
140 0           debug("trusted request from $origin to $target (earlier delegation)");
141             return
142 0           }
143             }
144              
145             # remove cookies, because there is no cross-domain trust
146             # we should remove authorization header too, but then access to the
147             # protected site will probably not be available at all (see BUGS section)
148 0           my @del;
149 0           push @del,$1 while ( $hdr =~s{^(Cookie|Cookie2):[ \t]*(.*(?:\n[ \t].*)*)\n}{}im );
150 0 0         if (@del) {
151 0           $self->run_callback([
152             IMP_LOG,0,0,0,IMP_LOG_INFO,
153             "removed cross-origin session credentials (@del) for request @origin -> @target"
154             ]);
155             # return changed header
156 0           return $hdr;
157             }
158              
159             # nothing changed
160 0           return undef;
161             }
162              
163             # find out if response header contains delegation through a redirect
164             sub _analyze_rphdr {
165 0     0     my ($self,$hdr) = @_;
166             # we are only interested in temporal redirects
167 0 0         $hdr =~m{\AHTTP/1\.[01] 30[237]} or return;
168              
169 0 0         my @location = _gethdr($hdr,'Location',$rx_host_from_url)
170             or return; # no redirect
171 0 0         if ( @location > 1 ) {
172             # invalid: multiple conflicting redirects
173 0           $self->run_callback(
174             [ IMP_LOG,0,0,0,IMP_LOG_WARNING,
175             "conflicting redirects in response\n".$hdr ],
176             [ IMP_DENY,0, "conflicting redirects in response" ]
177             );
178 0           return;
179             }
180 0           my $location = $location[0];
181 0 0         my $target = $self->{target} or return;
182 0 0         return if $target eq $location; # no cross-domain
183              
184 0           $target = _rootdom($target);
185 0           $location = _rootdom($location);
186 0 0         return if $target eq $location; # not considered cross-domain too
187              
188 0           _delegate($target,$location,'redirect');
189             }
190              
191             sub _gethdr {
192 0     0     my ($hdr,$key,$rx) = @_;
193 0           my @val;
194 0           for ( $hdr =~m{^\Q$key\E:[ \t]*(.*(?:\n[ \t].*)*)}mgi ) {
195 0           s{\r\n}{}g;
196 0           s{\s+$}{};
197 0           s{^\s+}{};
198 0           push @val, m{$rx};
199             }
200 0           my %v;
201 0           return grep { ! $v{$_}++ } @val;
  0            
202             }
203              
204             BEGIN {
205 1 50   1   6 if ( eval { require WWW::CSP::PublicDNSSuffix } ) {
  1 50       237  
    50          
206             *_rootdom = sub {
207 0         0 my ($rest,$tld) = WWW::CSP::PublicDNSSuffix::public_suffix( shift );
208 0 0       0 return $rest =~m{([^.]+)$} ? "$1.$tld" : undef;
209             }
210 0         0 } elsif ( eval { require Mozilla::PublicSuffix }) {
  1         232  
211             *_rootdom = sub {
212 0         0 my $host = shift;
213 0         0 my $suffix = Mozilla::PublicSuffix::public_suffix($host);
214 0 0       0 return $host =~m{([^\.]+\.\Q$suffix)} ? $1:undef,
215             }
216 0         0 } elsif ( my $suffix = eval {
217 1         376 require Domain::PublicSuffix;
218 0         0 Domain::PublicSuffix->new
219             }) {
220 0         0 *_rootdom = sub { return $suffix->get_root_domain( shift ) }
221 0         0 } else {
222 1         40 die "need one of Domain::PublicSuffix, Mozilla::PublicSuffix or WWW::CSP::PublicDNSSuffix"
223             }
224             }
225              
226             1;
227             __END__