File: 0010-Apply-fix-for-CVE-2024-28054.patch

package info (click to toggle)
amavisd-new 1%3A2.13.0-3%2Bdeb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 7,496 kB
  • sloc: perl: 33,813; sh: 480; sql: 158; makefile: 15
file content (213 lines) | stat: -rw-r--r-- 8,993 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
From: Brian May <brian@linuxpenguins.xyz>
Date: Tue, 12 Mar 2024 09:34:58 +1100
Subject: Apply fix for CVE-2024-28054

---
 README_FILES/README.CVE-2024-28054 | 54 ++++++++++++++++++++++++++++++++++++++
 conf/amavisd.conf                  |  1 +
 lib/Amavis.pm                      |  8 +++---
 lib/Amavis/Conf.pm                 |  2 ++
 lib/Amavis/Unpackers.pm            |  5 ++--
 lib/Amavis/Unpackers/MIME.pm       | 19 ++++++++++++++
 lib/Amavis/Unpackers/Part.pm       |  3 ++-
 7 files changed, 86 insertions(+), 6 deletions(-)
 create mode 100644 README_FILES/README.CVE-2024-28054

diff --git a/README_FILES/README.CVE-2024-28054 b/README_FILES/README.CVE-2024-28054
new file mode 100644
index 0000000..757c5c1
--- /dev/null
+++ b/README_FILES/README.CVE-2024-28054
@@ -0,0 +1,54 @@
+# Problem description
+
+Emails which consist of multiple parts (`Content-Type: multipart/*`)
+incorporate boundary information stating at which point one part ends and the
+next part begins.
+
+A boundary is announced by an Content-Type header's `boundary` parameter. To
+our current knowledge, RFC2046 and RFC2045 do not explicitly specify how a
+parser should handle multiple boundary parameters that contain conflicting
+values. As a result, there is no canonical choice which of the values should or
+should not be used for mime part decomposition.
+
+It turns out that MIME::Parser from MIME-tools chooses the last `boundary`
+parameter of a Content-Type-header, while several mail user agents choose the
+first occuring one. As a consequence, Amavis will apply some of its routines to
+content that a receiving MUA will not see, and vice-versa will not apply them
+to content that the receiving MUA will see. Such routines are at least
+- the banned-files check, and
+- the virus check, unless
+  - Amavis feeds the whole email into the virus scanner, and
+  - the virus scanner implements its own email parsing that aligns with the
+    receiving MUA's parser implementation.
+
+MIME::Parser does not provide a choice which of multiple `boundary` parameters
+shall be used for parsing, but it will give feedback in such a case [1], which
+Amavis can react to.
+Emails with ambiguous content, like multiple `boundary` parameters as described
+above, will be categorized as `CC_UNCHECKED,3`, since Amavis has no information
+about the recipient's MUA's parser implementation.
+
+# Recommendation
+
+Legitimate emails are not expected to have ambiguous content, so an Amavis setup
+should treat them harshly. The new default configuration for `CC_UNCHECKED,3` is
+defanging:
+
+```
+$defang_by_ccat{CC_UNCHECKED.",3"} = 1; # ambiguous content (e.g. multipart boundary)
+```
+
+Another possibility would be quarantining, e.g. via
+
+```
+$quarantine_to_maps_by_ccat{CC_UNCHECKED.",3"} = [1];
+$quarantine_method_by_ccat{CC_UNCHECKED.",3"} = 'local:unchecked-ambiguous-%m';
+```
+
+and/or discarding/rejecting the email:
+
+```
+$final_destiny_maps_by_ccat{CC_UNCHECKED.",3"} = D_REJECT; # or D_DISCARD
+```
+
+[1] https://metacpan.org/release/DSKOLL/MIME-tools-5.514/changes
diff --git a/conf/amavisd.conf b/conf/amavisd.conf
index f9de495..0a63cd9 100644
--- a/conf/amavisd.conf
+++ b/conf/amavisd.conf
@@ -148,6 +148,7 @@ $defang_banned = 1;  # MIME-wrap passed mail containing banned name
 $defang_by_ccat{CC_BADH.",3"} = 1;  # NUL or CR character in header
 $defang_by_ccat{CC_BADH.",5"} = 1;  # header line longer than 998 characters
 $defang_by_ccat{CC_BADH.",6"} = 1;  # header field syntax error
+$defang_by_ccat{CC_UNCHECKED.",3"} = 1; # ambiguous content (e.g. multipart boundary)
 
 # TLS verification example
 # $tls_security_level_out = 'may';
diff --git a/lib/Amavis.pm b/lib/Amavis.pm
index f49135f..5c7519b 100644
--- a/lib/Amavis.pm
+++ b/lib/Amavis.pm
@@ -2483,16 +2483,18 @@ sub check_mail($$) {
 
       $which_section = "parts_decode_ext";
       snmp_count('OpsDec');
-      my($any_encrypted,$over_levels);
-      ($hold, $any_undecipherable, $any_encrypted, $over_levels) =
+      my($any_encrypted,$over_levels,$ambiguous);
+      ($hold, $any_undecipherable, $any_encrypted, $over_levels, $ambiguous) =
         Amavis::Unpackers::decompose_mail($msginfo->mail_tempdir,
                                           $file_generator_object);
-      $any_undecipherable ||= ($any_encrypted || $over_levels);
+      $any_undecipherable ||= ($any_encrypted || $over_levels || $ambiguous);
       if ($any_undecipherable) {
         $msginfo->add_contents_category(CC_UNCHECKED,0);
         $msginfo->add_contents_category(CC_UNCHECKED,1) if $any_encrypted;
         $msginfo->add_contents_category(CC_UNCHECKED,2) if $over_levels;
+        $msginfo->add_contents_category(CC_UNCHECKED,3) if $ambiguous;
         for my $r (@{$msginfo->per_recip_data}) {
+          $r->add_contents_category(CC_UNCHECKED,3) if $ambiguous;
           next if $r->bypass_virus_checks;
           $r->add_contents_category(CC_UNCHECKED,0);
           $r->add_contents_category(CC_UNCHECKED,1) if $any_encrypted;
diff --git a/lib/Amavis/Conf.pm b/lib/Amavis/Conf.pm
index 7259cca..77fd671 100644
--- a/lib/Amavis/Conf.pm
+++ b/lib/Amavis/Conf.pm
@@ -1134,6 +1134,7 @@ BEGIN {
     CC_UNCHECKED,      'Unchecked',
     CC_UNCHECKED.',1', 'UncheckedEncrypted',
     CC_UNCHECKED.',2', 'UncheckedOverLimits',
+    CC_UNCHECKED.',3', 'UncheckedAmbiguousContent',
     CC_BANNED,     'Banned',
     CC_VIRUS,      'Virus',
   );
@@ -1608,6 +1609,7 @@ BEGIN {
     CC_BANNED,      'id=%n - BANNED: %F',
     CC_UNCHECKED.',1', 'id=%n - UNCHECKED: encrypted',
     CC_UNCHECKED.',2', 'id=%n - UNCHECKED: over limits',
+    CC_UNCHECKED.',3', 'id=%n - UNCHECKED: ambiguous content',
     CC_UNCHECKED,      'id=%n - UNCHECKED',
     CC_SPAM,        'id=%n - spam',
     CC_SPAMMY.',1', 'id=%n - spammy (tag3)',
diff --git a/lib/Amavis/Unpackers.pm b/lib/Amavis/Unpackers.pm
index 8d6684b..f36edb5 100644
--- a/lib/Amavis/Unpackers.pm
+++ b/lib/Amavis/Unpackers.pm
@@ -280,7 +280,7 @@ sub decompose_mail($$) {
   my($tempdir,$file_generator_object) = @_;
 
   my $hold; my(@parts); my $depth = 1;
-  my($any_undecipherable, $any_encrypted, $over_levels) = (0,0,0);
+  my($any_undecipherable, $any_encrypted, $over_levels, $ambiguous) = (0,0,0,0);
   my $which_section = "parts_decode";
   # fetch all not-yet-visited part names, and start a new cycle
 TIER:
@@ -342,13 +342,14 @@ TIER:
       if (defined $attr) {
         $any_undecipherable++  if index($attr, 'U') >= 0;
         $any_encrypted++       if index($attr, 'C') >= 0;
+        $ambiguous++           if index($attr, 'B') >= 0;
       }
     }
     last TIER  if defined $hold;
     $depth++;
   }
   section_time($which_section); prolong_timer($which_section);
-  ($hold, $any_undecipherable, $any_encrypted, $over_levels);
+  ($hold, $any_undecipherable, $any_encrypted, $over_levels, $ambiguous);
 }
 
 # Decompose one part
diff --git a/lib/Amavis/Unpackers/MIME.pm b/lib/Amavis/Unpackers/MIME.pm
index 181421f..49e035e 100644
--- a/lib/Amavis/Unpackers/MIME.pm
+++ b/lib/Amavis/Unpackers/MIME.pm
@@ -59,6 +59,24 @@ sub mime_decode_pre_epi($$$$$) {
   }
 }
 
+sub ambiguous_content {
+  my $entity = shift;
+  if ($entity->can('ambiguous_content')) {
+    return $entity->ambiguous_content;
+  } else {
+    return unless $entity->is_multipart;
+    my $content_type = $entity->head->get('Content-Type');
+    if ($content_type && $content_type =~ m{^multipart/\w+(.+)}x) {
+      my ($params, $num) = ($1, 0);
+      while ($params =~ m{\G ; \s+ (?<param>\w+) = (?: \w+ | "(?:\\.|[^"\\])*" )}gx) {
+        $num++ if lc($+{param}) eq 'boundary';
+      }
+      return $num > 1;
+    }
+    return;
+  }
+}
+
 # traverse MIME::Entity object depth-first,
 # extracting preambles and epilogues as extra (pseudo)parts, and
 # filling-in additional information into Amavis::Unpackers::Part objects
@@ -73,6 +91,7 @@ sub mime_traverse($$$$$) {
   if (!defined($body)) {  # a MIME container only contains parts, no bodypart
     # create pseudo-part objects for MIME containers (e.g. multipart/* )
     $part = Amavis::Unpackers::Part->new(undef,$parent_obj,1);
+    $part->attributes_add('B') if ambiguous_content($entity);
 #   $part->type_short('no-file');
     do_log(2, "%s %s Content-Type: %s", $part->base_name, $placement, $mt);
 
diff --git a/lib/Amavis/Unpackers/Part.pm b/lib/Amavis/Unpackers/Part.pm
index 0cb188e..83c1450 100644
--- a/lib/Amavis/Unpackers/Part.pm
+++ b/lib/Amavis/Unpackers/Part.pm
@@ -65,7 +65,8 @@ sub exists
 sub attributes        # a string of characters representing attributes
   { @_<2 ? shift->{attr}     : ($_[0]->{attr} = $_[1]) };
 
-sub attributes_add {  # U=undecodable, C=crypted, D=directory,S=special,L=link
+sub attributes_add {  # U=undecodable, C=crypted, B=ambiguous-content,
+                      # D=directory, S=special, L=link
   my $self = shift; my $a = $self->{attr}; $a = '' if !defined $a;
   for my $arg (@_) { $a .= $arg  if $arg ne '' && index($a,$arg) < 0 }
   $self->{attr} = $a;