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__ |