File Coverage

blib/lib/Mail/Milter/Authentication/Handler/Sanitize.pm
Criterion Covered Total %
statement 104 118 88.1
branch 30 40 75.0
condition n/a
subroutine 15 18 83.3
pod 2 12 16.6
total 151 188 80.3


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Handler::Sanitize;
2 34     34   23884 use 5.20.0;
  34         211  
3 34     34   252 use strict;
  34         113  
  34         874  
4 34     34   182 use warnings;
  34         171  
  34         1118  
5 34     34   229 use Mail::Milter::Authentication::Pragmas;
  34         115  
  34         463  
6             # ABSTRACT: Handler class for Removing headers
7             our $VERSION = '3.20230629'; # VERSION
8 34     34   8961 use base 'Mail::Milter::Authentication::Handler';
  34         135  
  34         4928  
9 34     34   411 use List::MoreUtils qw{ uniq };
  34         135  
  34         1587  
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 403 'sanitize_remove_total' => 'The number Authentication Results headers removed',
29             };
30             }
31              
32             sub is_hostname_mine {
33 32     32 0 85 my ( $self, $check_hostname ) = @_;
34 32         130 my $config = $self->handler_config();
35              
36 32 50       108 return 0 if ! defined $check_hostname;
37              
38 32 50       106 if ( exists( $config->{'hosts_to_remove'} ) ) {
39 32         62 foreach my $remove_hostname ( @{ $config->{'hosts_to_remove'} } ) {
  32         122  
40 32 100       463 if ( $check_hostname =~ m/^(.*\.)?\Q${remove_hostname}\E$/i ) {
41 8         44 return 1;
42             }
43             }
44             }
45              
46 24         140 my $hostname = $self->get_my_hostname();
47 24         170 my ($check_for) = $hostname =~ /^[^\.]+\.(.*)/;
48 24 100       287 if ( $check_hostname =~ m/^(.*\.)?\Q${check_for}\E$/i ) {
49 8         39 return 1;
50             }
51              
52 16         133 my $authserv_id = $self->get_my_authserv_id();
53 16 100       71 if ( fc( $check_hostname ) eq fc( $authserv_id ) ) {
54 2         50 return 1;
55             }
56              
57 14         51 return 0;
58             }
59              
60             sub remove_auth_header {
61 18     18 0 58 my ( $self, $index, $type ) = @_;
62 18         162 $self->metric_count( 'sanitize_remove_total', {'header'=>$type} );
63 18 100       99 if ( !exists( $self->{'remove_auth_headers'}->{$type} ) ) {
64 4         29 $self->{'remove_auth_headers'}->{$type} = [];
65             }
66 18         78 push @{ $self->{'remove_auth_headers'}->{$type} }, $index;
  18         86  
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 2388 my ( $self ) = @_;
81 993         4025 my @headers = sort keys $headers_to_remove->%*;
82 993         4088 return \@headers;
83             }
84              
85             sub get_remove_header_settings {
86 74     74 0 267 my ($self, $key) = @_;
87 74         934 return $headers_to_remove->{lc $key};
88             }
89             }
90              
91             sub envfrom_callback {
92 74     74 0 471 my ( $self, $env_from ) = @_;
93 74         557 $self->{'auth_result_header_index'} = {};
94 74         393 $self->{'remove_auth_headers'} = {};
95              
96 74         307 my $headers = {};
97 74         238 foreach my $header ( sort @{ $self->get_headers_to_remove() } ) {
  74         425  
98             $headers->{ lc $header } = {
99             'index' => 0,
100             'silent' => $self->get_remove_header_settings($header)->{silent},
101 74         460 };
102             }
103 74         506 $self->{'header_hash'} = $headers;
104             }
105              
106             sub header_callback {
107 875     875 0 2811 my ( $self, $header, $value ) = @_;
108 875         3186 my $config = $self->handler_config();
109              
110 875 100       3073 return if ( $self->is_trusted_ip_address() );
111 845 50       3234 return if ( lc $config->{'remove_headers'} eq 'no' );
112              
113 845         2395 my @types = ('Authentication-Results');
114 845 50       2598 if ( exists $config->{extra_auth_results_types} ) {
115 0         0 push @types, $config->{extra_auth_results_types}->@*;
116             }
117 845         5733 for my $type (uniq sort @types) {
118              
119             # Sanitize Authentication-Results headers
120 845 100       2999 if ( lc $header eq lc $type ) {
121 32 100       142 if ( !exists $self->{'auth_result_header_index'}->{$type} ) {
122 4         20 $self->{'auth_result_header_index'}->{$type} = 0;
123             }
124             $self->{'auth_result_header_index'}->{$type} =
125 32         97 $self->{'auth_result_header_index'}->{$type} + 1;
126              
127 32         87 my $authserv_id = '';
128 32         77 eval {
129 32         289 my $parsed = Mail::AuthenticationResults::Parser->new()->parse($value);
130 32         131885 $authserv_id = $parsed->value()->value();
131             };
132 32 50       872 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         75 my $remove = 0;
138 32         116 my $silent = lc $config->{'remove_headers'} eq 'silent';
139 32 50       98 if ( $authserv_id ) {
140 32         142 $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       121 if ( $remove ) {
150 18         98 $self->remove_auth_header( $self->{'auth_result_header_index'}->{$type}, $type );
151 18 50       61 if ( ! $silent ) {
152 18         124 my $forged_header =
153             '(Received '.$type.' header removed by '
154             . $self->get_my_hostname()
155             . ')' . "\n"
156             . ' '
157             . $value;
158 18         175 $self->append_header( 'X-Received-'.$type,
159             $forged_header );
160             }
161             }
162             }
163             }
164              
165             # Sanitize other headers
166 845         2411 foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) {
  845         2505  
167 845 100       4242 next if ( lc $remove_header ne lc $header );
168 8         81 $self->{'header_hash'}->{ lc $header }->{'index'} = $self->{'header_hash'}->{ lc $header }->{'index'} + 1;
169 8         67 $self->metric_count( 'sanitize_remove_total', {'header'=> lc $header} );
170              
171 8 50       92 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 360 my ($self) = @_;
186 74         354 my $config = $self->handler_config();
187 74 50       755 return if ( lc $config->{'remove_headers'} eq 'no' );
188              
189 74 50       433 if ( exists( $self->{'remove_auth_headers'} ) ) {
190 74         651 foreach my $type ( sort keys $self->{'remove_auth_headers'}->%* ) {
191 4         16 foreach my $index ( reverse @{ $self->{'remove_auth_headers'}->{$type} } ) {
  4         20  
192 18         224 $self->dbgout( 'RemoveAuthHeader', "$type $index", LOG_DEBUG );
193 18         183 $self->change_header( $type, $index, q{} );
194             }
195             }
196             }
197              
198 74         329 foreach my $remove_header ( sort @{ $self->get_headers_to_remove() } ) {
  74         391  
199 74         520 my $max_index = $self->{'header_hash'}->{ lc $remove_header }->{'index'};
200 74 100       423 if ( $max_index ) {
201 4         29 for ( my $index = $max_index; $index > 0; $index-- ) {
202 8         107 $self->dbgout( 'RemoveHeader', "$remove_header $index", LOG_DEBUG );
203 8         41 $self->change_header( $remove_header, $index, q{} );
204             }
205             }
206             }
207             }
208              
209             sub close_callback {
210 117     117 0 494 my ( $self ) = @_;
211 117         486 delete $self->{'remove_auth_headers'};
212 117         377 delete $self->{'auth_result_header_index'};
213 117         560 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.20230629
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