line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Mail::Milter::Authentication::Handler::Sanitize; |
2
|
34
|
|
|
34
|
|
21391
|
use 5.20.0; |
|
34
|
|
|
|
|
169
|
|
3
|
34
|
|
|
34
|
|
269
|
use strict; |
|
34
|
|
|
|
|
107
|
|
|
34
|
|
|
|
|
766
|
|
4
|
34
|
|
|
34
|
|
192
|
use warnings; |
|
34
|
|
|
|
|
110
|
|
|
34
|
|
|
|
|
1095
|
|
5
|
34
|
|
|
34
|
|
346
|
use Mail::Milter::Authentication::Pragmas; |
|
34
|
|
|
|
|
98
|
|
|
34
|
|
|
|
|
450
|
|
6
|
|
|
|
|
|
|
# ABSTRACT: Handler class for Removing headers |
7
|
|
|
|
|
|
|
our $VERSION = '3.20230911'; # VERSION |
8
|
34
|
|
|
34
|
|
8823
|
use base 'Mail::Milter::Authentication::Handler'; |
|
34
|
|
|
|
|
85
|
|
|
34
|
|
|
|
|
3888
|
|
9
|
34
|
|
|
34
|
|
269
|
use List::MoreUtils qw{ uniq }; |
|
34
|
|
|
|
|
145
|
|
|
34
|
|
|
|
|
1946
|
|
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
sub default_config { |
12
|
|
|
|
|
|
|
return { |
13
|
0
|
|
|
0
|
0
|
0
|
'hosts_to_remove' => [ 'example.com', 'example.net' ], |
14
|
|
|
|
|
|
|
'remove_headers' => 'yes', |
15
|
|
|
|
|
|
|
'extra_auth_results_types' => ['X-Authentication-Results'], |
16
|
|
|
|
|
|
|
}; |
17
|
|
|
|
|
|
|
} |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
sub grafana_rows { |
20
|
0
|
|
|
0
|
0
|
0
|
my ( $self ) = @_; |
21
|
0
|
|
|
|
|
0
|
my @rows; |
22
|
0
|
|
|
|
|
0
|
push @rows, $self->get_json( 'Sanitize_metrics' ); |
23
|
0
|
|
|
|
|
0
|
return \@rows; |
24
|
|
|
|
|
|
|
} |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
sub register_metrics { |
27
|
|
|
|
|
|
|
return { |
28
|
33
|
|
|
33
|
1
|
329
|
'sanitize_remove_total' => 'The number Authentication Results headers removed', |
29
|
|
|
|
|
|
|
}; |
30
|
|
|
|
|
|
|
} |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
sub is_hostname_mine { |
33
|
32
|
|
|
32
|
0
|
81
|
my ( $self, $check_hostname ) = @_; |
34
|
32
|
|
|
|
|
128
|
my $config = $self->handler_config(); |
35
|
|
|
|
|
|
|
|
36
|
32
|
50
|
|
|
|
104
|
return 0 if ! defined $check_hostname; |
37
|
|
|
|
|
|
|
|
38
|
32
|
50
|
|
|
|
97
|
if ( exists( $config->{'hosts_to_remove'} ) ) { |
39
|
32
|
|
|
|
|
59
|
foreach my $remove_hostname ( @{ $config->{'hosts_to_remove'} } ) { |
|
32
|
|
|
|
|
112
|
|
40
|
32
|
100
|
|
|
|
546
|
if ( $check_hostname =~ m/^(.*\.)?\Q${remove_hostname}\E$/i ) { |
41
|
8
|
|
|
|
|
41
|
return 1; |
42
|
|
|
|
|
|
|
} |
43
|
|
|
|
|
|
|
} |
44
|
|
|
|
|
|
|
} |
45
|
|
|
|
|
|
|
|
46
|
24
|
|
|
|
|
131
|
my $hostname = $self->get_my_hostname(); |
47
|
24
|
|
|
|
|
170
|
my ($check_for) = $hostname =~ /^[^\.]+\.(.*)/; |
48
|
24
|
100
|
|
|
|
277
|
if ( $check_hostname =~ m/^(.*\.)?\Q${check_for}\E$/i ) { |
49
|
8
|
|
|
|
|
36
|
return 1; |
50
|
|
|
|
|
|
|
} |
51
|
|
|
|
|
|
|
|
52
|
16
|
|
|
|
|
112
|
my $authserv_id = $self->get_my_authserv_id(); |
53
|
16
|
100
|
|
|
|
114
|
if ( fc( $check_hostname ) eq fc( $authserv_id ) ) { |
54
|
2
|
|
|
|
|
8
|
return 1; |
55
|
|
|
|
|
|
|
} |
56
|
|
|
|
|
|
|
|
57
|
14
|
|
|
|
|
47
|
return 0; |
58
|
|
|
|
|
|
|
} |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
sub remove_auth_header { |
61
|
18
|
|
|
18
|
0
|
54
|
my ( $self, $index, $type ) = @_; |
62
|
18
|
|
|
|
|
132
|
$self->metric_count( 'sanitize_remove_total', {'header'=>$type} ); |
63
|
18
|
100
|
|
|
|
104
|
if ( !exists( $self->{'remove_auth_headers'}->{$type} ) ) { |
64
|
4
|
|
|
|
|
22
|
$self->{'remove_auth_headers'}->{$type} = []; |
65
|
|
|
|
|
|
|
} |
66
|
18
|
|
|
|
|
44
|
push @{ $self->{'remove_auth_headers'}->{$type} }, $index; |
|
18
|
|
|
|
|
71
|
|
67
|
|
|
|
|
|
|
} |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
{ |
70
|
|
|
|
|
|
|
my $headers_to_remove = { |
71
|
|
|
|
|
|
|
'x-disposition-quarantine' => { silent => 1 }, |
72
|
|
|
|
|
|
|
}; |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub add_header_to_sanitize_list { |
75
|
0
|
|
|
0
|
1
|
0
|
my ( $self, $header, $silent ) = @_; |
76
|
0
|
|
|
|
|
0
|
$headers_to_remove->{lc $header} = { silent => $silent }; |
77
|
|
|
|
|
|
|
} |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
sub get_headers_to_remove { |
80
|
993
|
|
|
993
|
0
|
2364
|
my ( $self ) = @_; |
81
|
993
|
|
|
|
|
4016
|
my @headers = sort keys $headers_to_remove->%*; |
82
|
993
|
|
|
|
|
4219
|
return \@headers; |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
sub get_remove_header_settings { |
86
|
74
|
|
|
74
|
0
|
298
|
my ($self, $key) = @_; |
87
|
74
|
|
|
|
|
1042
|
return $headers_to_remove->{lc $key}; |
88
|
|
|
|
|
|
|
} |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
sub envfrom_callback { |
92
|
74
|
|
|
74
|
0
|
426
|
my ( $self, $env_from ) = @_; |
93
|
74
|
|
|
|
|
518
|
$self->{'auth_result_header_index'} = {}; |
94
|
74
|
|
|
|
|
397
|
$self->{'remove_auth_headers'} = {}; |
95
|
|
|
|
|
|
|
|
96
|
74
|
|
|
|
|
228
|
my $headers = {}; |
97
|
74
|
|
|
|
|
222
|
foreach my $header ( sort @{ $self->get_headers_to_remove() } ) { |
|
74
|
|
|
|
|
457
|
|
98
|
|
|
|
|
|
|
$headers->{ lc $header } = { |
99
|
|
|
|
|
|
|
'index' => 0, |
100
|
|
|
|
|
|
|
'silent' => $self->get_remove_header_settings($header)->{silent}, |
101
|
74
|
|
|
|
|
487
|
}; |
102
|
|
|
|
|
|
|
} |
103
|
74
|
|
|
|
|
524
|
$self->{'header_hash'} = $headers; |
104
|
|
|
|
|
|
|
} |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
sub header_callback { |
107
|
875
|
|
|
875
|
0
|
2603
|
my ( $self, $header, $value ) = @_; |
108
|
875
|
|
|
|
|
2966
|
my $config = $self->handler_config(); |
109
|
|
|
|
|
|
|
|
110
|
875
|
100
|
|
|
|
3004
|
return if ( $self->is_trusted_ip_address() ); |
111
|
845
|
50
|
|
|
|
2991
|
return if ( lc $config->{'remove_headers'} eq 'no' ); |
112
|
|
|
|
|
|
|
|
113
|
845
|
|
|
|
|
2093
|
my @types = ('Authentication-Results'); |
114
|
845
|
50
|
|
|
|
2366
|
if ( exists $config->{extra_auth_results_types} ) { |
115
|
0
|
|
|
|
|
0
|
push @types, $config->{extra_auth_results_types}->@*; |
116
|
|
|
|
|
|
|
} |
117
|
845
|
|
|
|
|
5894
|
for my $type (uniq sort @types) { |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
# Sanitize Authentication-Results headers |
120
|
845
|
100
|
|
|
|
3085
|
if ( lc $header eq lc $type ) { |
121
|
32
|
100
|
|
|
|
137
|
if ( !exists $self->{'auth_result_header_index'}->{$type} ) { |
122
|
4
|
|
|
|
|
27
|
$self->{'auth_result_header_index'}->{$type} = 0; |
123
|
|
|
|
|
|
|
} |
124
|
|
|
|
|
|
|
$self->{'auth_result_header_index'}->{$type} = |
125
|
32
|
|
|
|
|
95
|
$self->{'auth_result_header_index'}->{$type} + 1; |
126
|
|
|
|
|
|
|
|
127
|
32
|
|
|
|
|
106
|
my $authserv_id = ''; |
128
|
32
|
|
|
|
|
72
|
eval { |
129
|
32
|
|
|
|
|
322
|
my $parsed = Mail::AuthenticationResults::Parser->new()->parse($value); |
130
|
32
|
|
|
|
|
128370
|
$authserv_id = $parsed->value()->value(); |
131
|
|
|
|
|
|
|
}; |
132
|
32
|
50
|
|
|
|
858
|
if ( my $error = $@ ) { |
133
|
0
|
|
|
|
|
0
|
$self->handle_exception($error); |
134
|
0
|
|
|
|
|
0
|
$self->log_error("Error parsing existing Authentication-Results header: $value"); |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
32
|
|
|
|
|
77
|
my $remove = 0; |
138
|
32
|
|
|
|
|
100
|
my $silent = lc $config->{'remove_headers'} eq 'silent'; |
139
|
32
|
50
|
|
|
|
92
|
if ( $authserv_id ) { |
140
|
32
|
|
|
|
|
130
|
$remove = $self->is_hostname_mine($authserv_id); |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
else { |
143
|
|
|
|
|
|
|
# We couldn't parse the authserv_id, removing this header is the safest option |
144
|
|
|
|
|
|
|
# Add to X-Received headers for analysis later |
145
|
0
|
|
|
|
|
0
|
$remove = 1; |
146
|
0
|
|
|
|
|
0
|
$silent = 0; |
147
|
|
|
|
|
|
|
} |
148
|
|
|
|
|
|
|
|
149
|
32
|
100
|
|
|
|
106
|
if ( $remove ) { |
150
|
18
|
|
|
|
|
95
|
$self->remove_auth_header( $self->{'auth_result_header_index'}->{$type}, $type ); |
151
|
18
|
50
|
|
|
|
60
|
if ( ! $silent ) { |
152
|
18
|
|
|
|
|
119
|
my $forged_header = |
153
|
|
|
|
|
|
|
'(Received '.$type.' header removed by ' |
154
|
|
|
|
|
|
|
. $self->get_my_hostname() |
155
|
|
|
|
|
|
|
. ')' . "\n" |
156
|
|
|
|
|
|
|
. ' ' |
157
|
|
|
|
|
|
|
. $value; |
158
|
18
|
|
|
|
|
149
|
$self->append_header( 'X-Received-'.$type, |
159
|
|
|
|
|
|
|
$forged_header ); |
160
|
|
|
|
|
|
|
} |
161
|
|
|
|
|
|
|
} |
162
|
|
|
|
|
|
|
} |
163
|
|
|
|
|
|
|
} |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
# Sanitize other headers |
166
|
845
|
|
|
|
|
2399
|
foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) { |
|
845
|
|
|
|
|
2361
|
|
167
|
845
|
100
|
|
|
|
4161
|
next if ( lc $remove_header ne lc $header ); |
168
|
8
|
|
|
|
|
94
|
$self->{'header_hash'}->{ lc $header }->{'index'} = $self->{'header_hash'}->{ lc $header }->{'index'} + 1; |
169
|
8
|
|
|
|
|
65
|
$self->metric_count( 'sanitize_remove_total', {'header'=> lc $header} ); |
170
|
|
|
|
|
|
|
|
171
|
8
|
50
|
|
|
|
79
|
if ( ! $self->{'header_hash'}->{ lc $header }->{'silent'} ) { |
172
|
0
|
|
|
|
|
0
|
my $forged_header = |
173
|
|
|
|
|
|
|
'(Received ' . $remove_header . ' header removed by ' |
174
|
|
|
|
|
|
|
. $self->get_my_hostname() |
175
|
|
|
|
|
|
|
. ')' . "\n" |
176
|
|
|
|
|
|
|
. ' ' |
177
|
|
|
|
|
|
|
. $value; |
178
|
0
|
|
|
|
|
0
|
$self->append_header( 'X-Received-' . $remove_header, |
179
|
|
|
|
|
|
|
$forged_header ); |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
} |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
sub eom_callback { |
185
|
74
|
|
|
74
|
0
|
342
|
my ($self) = @_; |
186
|
74
|
|
|
|
|
343
|
my $config = $self->handler_config(); |
187
|
74
|
50
|
|
|
|
817
|
return if ( lc $config->{'remove_headers'} eq 'no' ); |
188
|
|
|
|
|
|
|
|
189
|
74
|
50
|
|
|
|
366
|
if ( exists( $self->{'remove_auth_headers'} ) ) { |
190
|
74
|
|
|
|
|
625
|
foreach my $type ( sort keys $self->{'remove_auth_headers'}->%* ) { |
191
|
4
|
|
|
|
|
16
|
foreach my $index ( reverse @{ $self->{'remove_auth_headers'}->{$type} } ) { |
|
4
|
|
|
|
|
19
|
|
192
|
18
|
|
|
|
|
234
|
$self->dbgout( 'RemoveAuthHeader', "$type $index", LOG_DEBUG ); |
193
|
18
|
|
|
|
|
213
|
$self->change_header( $type, $index, q{} ); |
194
|
|
|
|
|
|
|
} |
195
|
|
|
|
|
|
|
} |
196
|
|
|
|
|
|
|
} |
197
|
|
|
|
|
|
|
|
198
|
74
|
|
|
|
|
264
|
foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) { |
|
74
|
|
|
|
|
340
|
|
199
|
74
|
|
|
|
|
556
|
my $max_index = $self->{'header_hash'}->{ lc $remove_header }->{'index'}; |
200
|
74
|
100
|
|
|
|
430
|
if ( $max_index ) { |
201
|
4
|
|
|
|
|
28
|
for ( my $index = $max_index; $index > 0; $index-- ) { |
202
|
8
|
|
|
|
|
97
|
$self->dbgout( 'RemoveHeader', "$remove_header $index", LOG_DEBUG ); |
203
|
8
|
|
|
|
|
39
|
$self->change_header( $remove_header, $index, q{} ); |
204
|
|
|
|
|
|
|
} |
205
|
|
|
|
|
|
|
} |
206
|
|
|
|
|
|
|
} |
207
|
|
|
|
|
|
|
} |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
sub close_callback { |
210
|
117
|
|
|
117
|
0
|
414
|
my ( $self ) = @_; |
211
|
117
|
|
|
|
|
495
|
delete $self->{'remove_auth_headers'}; |
212
|
117
|
|
|
|
|
373
|
delete $self->{'auth_result_header_index'}; |
213
|
117
|
|
|
|
|
570
|
delete $self->{'header_hash'}; |
214
|
|
|
|
|
|
|
} |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
1; |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
__END__ |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
=pod |
221
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
=encoding UTF-8 |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
=head1 NAME |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
Mail::Milter::Authentication::Handler::Sanitize - Handler class for Removing headers |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
=head1 VERSION |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
version 3.20230911 |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=head1 DESCRIPTION |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
Remove unauthorized (forged) Authentication-Results headers from processed email. |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
=head1 CONFIGURATION |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
"Sanitize" : { | Config for the Sanitize Module |
239
|
|
|
|
|
|
|
| Remove conflicting Auth-results headers from inbound mail |
240
|
|
|
|
|
|
|
"hosts_to_remove" : [ | Hostnames (including subdomains thereof) for which we |
241
|
|
|
|
|
|
|
"example.com", | want to remove existing authentication results headers. |
242
|
|
|
|
|
|
|
"example.net" |
243
|
|
|
|
|
|
|
], |
244
|
|
|
|
|
|
|
"remove_headers" : "yes", | Remove headers with conflicting host names (as defined above) |
245
|
|
|
|
|
|
|
| "no" : do not remove |
246
|
|
|
|
|
|
|
| "yes" : remove and add a header for each one |
247
|
|
|
|
|
|
|
| "silent" : remove silently |
248
|
|
|
|
|
|
|
| Does not run for trusted IP address connections |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
"extra_auth_results_types" : [ | List of extra Authentication-Results style headers which we |
251
|
|
|
|
|
|
|
"X-Authentication-Results", | want to treat as Authentication-Results and sanitize. |
252
|
|
|
|
|
|
|
], |
253
|
|
|
|
|
|
|
} |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
=head1 AUTHOR |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
Marc Bradshaw <marc@marcbradshaw.net> |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
260
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
This software is copyright (c) 2020 by Marc Bradshaw. |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
264
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=cut |