CVE-2024-4140: Insufficient Information (NVD-CWE-noinfo)
value: An excessive memory use issue (CWE-770) exists in Email-MIME, before version 1.954, which can cause denial of service when parsing multipart MIME messages. The patch set (from 2020 and 2024) limits excessive depth and the total number of parts.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-02T20:15Z
Repository: https://github.com/rjbs/Email-MIME
Language: Perl
Files changed: lib/Email/MIME.pm
Commit date: 2024-04-22 20:24:09 -0400
Commit message: do not allow an excessive number of parts

This is annoying!  It bugs me that making a mere 100,000 parts should lead
to excessive consumption of memory… but really, nobody does that for
a good reason.

This makes top-level (non-recursive) Email::MIME->new set a localized
part counter to 0 and increment before it makes new subpart objects.
If the counter exceeds the max, it will die early.

It would be better to work, but this is better than eating all memory and
crashing, right?

Put together with help from Marc Bradshaw.

Test forthcoming.

Diff: diff --git a/lib/Email/MIME.pm b/lib/Email/MIME.pm
index 0921c1f..fb2dbe4 100644
--- a/lib/Email/MIME.pm
+++ b/lib/Email/MIME.pm
@@ -16,10 +16,15 @@ use Email::MIME::Header;
 use Encode 1.9801 ();
 use Scalar::Util qw(reftype weaken);
 
 our @CARP_NOT = qw(Email::MIME::ContentType);
 
+our $MAX_DEPTH = 10;
+
+our $CUR_PARTS = 0;
+our $MAX_PARTS = 100;
+
 =head1 SYNOPSIS
 
 B<Wait!>  Before you read this, maybe you just need L<Email::Stuffer>, which is
 a much easier-to-use tool for building simple email messages that might have
 attachments or both plain text and HTML.  If that doesn't do it for you, then
@@ -121,10 +126,16 @@ my $NO_ENCODE_RE = qr/
   \A
   (?:7bit|8bit|binary)\s*(?:;|$)
 /ix;
 
 sub new {
+  local $CUR_PARTS = 0;
+  my ($class, @rest) = @_;
+  $class->_new(@rest);
+}
+
+sub _new {
   my ($class, $text, $arg, @rest) = @_;
   $arg ||= {};
 
   my $encode_check = exists $arg->{encode_check}
                    ? delete $arg->{encode_check}
@@ -372,12 +383,10 @@ sub body_str {
 
   my $str = Encode::decode($encoding, $self->body, $self->encode_check);
   return $str;
 }
 
-our $MAX_DEPTH = 10;
-
 sub parts_multipart {
   my $self     = shift;
   my $boundary = $self->{ct}->{attributes}->{boundary};
 
   our $DEPTH ||= 0;
@@ -406,18 +415,20 @@ sub parts_multipart {
   # body.  This is a horrible hack, although it's debatable whether it was
   # better or worse when it was $self->{body} = shift @bits ... -- rjbs,
   # 2006-11-27
   $self->SUPER::body_set(shift @bits) if index(($bits[0] || ''), ':') == -1;
 
-  my $bits = @bits;
+  local $CUR_PARTS = $CUR_PARTS + @bits;
+  Carp::croak("attempted to parse a MIME message with more than $MAX_PARTS parts")
+    if $MAX_PARTS && $CUR_PARTS > $MAX_PARTS;
 
   my @parts;
   for my $bit (@bits) {
     $bit =~ s/\A[\n\r]+//smg;
     $bit =~ s/(?<!\x0d)$self->{mycrlf}\Z//sm;
     local $DEPTH = $DEPTH + 1;
-    my $email = (ref $self)->new($bit, { encode_check => $self->encode_check });
+    my $email = (ref $self)->_new($bit, { encode_check => $self->encode_check });
     push @parts, $email;
   }
 
   $self->{parts} = \@parts;
 

Vulnerable code: our $MAX_DEPTH = 10;

  my $bits = @bits;
    my $email = (ref $self)->new($bit, { encode_check => $self->encode_check });
Fixed code: our $MAX_DEPTH = 10;

our $CUR_PARTS = 0;
our $MAX_PARTS = 100;

  local $CUR_PARTS = 0;
  my ($class, @rest) = @_;
  $class->_new(@rest);
}

sub _new {
  local $CUR_PARTS = $CUR_PARTS + @bits;
  Carp::croak("attempted to parse a MIME message with more than $MAX_PARTS parts")
    if $MAX_PARTS && $CUR_PARTS > $MAX_PARTS;
    my $email = (ref $self)->_new($bit, { encode_check => $self->encode_check });