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