File CVE-2024-27305.patch of Package python-aiosmtpd.18391

Index: aiosmtpd-1.2.1/aiosmtpd/smtp.py
===================================================================
--- aiosmtpd-1.2.1.orig/aiosmtpd/smtp.py
+++ aiosmtpd-1.2.1/aiosmtpd/smtp.py
@@ -3,6 +3,7 @@ import socket
 import asyncio
 import logging
 import collections
+import enum
 
 from asyncio import sslproto
 from email._header_value_parser import get_addr_spec, get_angle_addr
@@ -12,13 +13,20 @@ from warnings import warn
 
 
 __version__ = '1.2+'
+
+
+class _DataState(enum.Enum):
+    NOMINAL = enum.auto()
+    TOO_LONG = enum.auto()
+    TOO_MUCH = enum.auto()
 __ident__ = 'Python SMTP {}'.format(__version__)
 log = logging.getLogger('mail.log')
 
 
 DATA_SIZE_DEFAULT = 33554432
+EMPTY_BARR = bytearray()
 EMPTYBYTES = b''
-NEWLINE = '\n'
+NEWLINE = '\r\n'
 MISSING = object()
 
 
@@ -65,6 +73,11 @@ class SMTP(asyncio.StreamReaderProtocol)
     command_size_limit = 512
     command_size_limits = collections.defaultdict(
         lambda x=command_size_limit: x)
+    line_length_limit = 1001
+    """Maximum line length according to RFC 5321 s 4.5.3.1.6"""
+    # The number comes from this calculation:
+    # (RFC 5322 s 2.1.1 + RFC 6532 s 3.4) 998 octets + CRLF = 1000 octets
+    # (RFC 5321 s 4.5.3.1.6) 1000 octets + "transparent dot" = 1001 octets
 
     def __init__(self, handler,
                  *,
@@ -637,43 +650,84 @@ class SMTP(asyncio.StreamReaderProtocol)
             return
         await self.push('354 End data with <CR><LF>.<CR><LF>')
         data = []
+
         num_bytes = 0
-        size_exceeded = False
+        limit = self.data_size_limit
+        line_fragments = []
+        state = _DataState.NOMINAL
         while self.transport is not None:           # pragma: nobranch
+            # Since eof_received cancels this coroutine,
+            # readuntil() can never raise asyncio.IncompleteReadError.
             try:
-                line = await self._reader.readline()
+                # https://datatracker.ietf.org/doc/html/rfc5321#section-2.3.8
+                line = await self._reader.readuntil(b'\r\n')
                 log.debug('DATA readline: %s', line)
+                assert line.endswith(b'\r\n')
             except asyncio.CancelledError:
                 # The connection got reset during the DATA command.
                 log.info('Connection lost during DATA')
                 self._writer.close()
                 raise
-            if line == b'.\r\n':
-                if data:
-                    data[-1] = data[-1].rstrip(b'\r\n')
+            except asyncio.LimitOverrunError as e:
+                # The line exceeds StreamReader's "stream limit".
+                # Delay SMTP Status Code sending until data receive is complete
+                # This seems to be implied in RFC 5321 § 4.2.5
+                if state == _DataState.NOMINAL:
+                    # Transition to TOO_LONG only if we haven't gone TOO_MUCH yet
+                    state = _DataState.TOO_LONG
+                # Discard data immediately to prevent memory pressure
+                data *= 0
+                # Drain the stream anyways
+                line = await self._reader.read(e.consumed)
+                assert not line.endswith(b'\r\n')
+            # A lone dot in a line signals the end of DATA.
+            if not line_fragments and line == b'.\r\n':
                 break
             num_bytes += len(line)
-            if (not size_exceeded and
-                    self.data_size_limit and
-                    num_bytes > self.data_size_limit):
-                size_exceeded = True
+            if state == _DataState.NOMINAL and limit and num_bytes > limit:
+                # Delay SMTP Status Code sending until data receive is complete
+                # This seems to be implied in RFC 5321 § 4.2.5
+                state = _DataState.TOO_MUCH
+                # Discard data immediately to prevent memory pressure
+                data *= 0
+            line_fragments.append(line)
+            if line.endswith(b'\r\n'):
+                # Record data only if state is "NOMINAL"
+                if state == _DataState.NOMINAL:
+                    line = EMPTY_BARR.join(line_fragments)
+                    if len(line) > self.line_length_limit:
+                        # Theoretically we shouldn't reach this place. But it's always
+                        # good to practice DEFENSIVE coding.
+                        state = _DataState.TOO_LONG
+                        # Discard data immediately to prevent memory pressure
+                        data *= 0
+                    else:
+                        data.append(EMPTY_BARR.join(line_fragments))
+                line_fragments *= 0
+
+        # Day of reckoning! Let's take care of those out-of-nominal situations
+        if state != _DataState.NOMINAL:
+            if state == _DataState.TOO_LONG:
+                await self.push("500 Line too long (see RFC5321 4.5.3.1.6)")
+            elif state == _DataState.TOO_MUCH:  # pragma: nobranch
                 await self.push('552 Error: Too much mail data')
-            if not size_exceeded:
-                data.append(line)
-        if size_exceeded:
             self._set_post_data_state()
             return
+
+        # If unfinished_line is non-empty, then the connection was closed.
+        assert not line_fragments
+
         # Remove extraneous carriage returns and de-transparency
         # according to RFC 5321, Section 4.5.2.
-        for i in range(len(data)):
-            text = data[i]
-            if text and text[:1] == b'.':
-                data[i] = text[1:]
+        for text in data:
+            if text.startswith(b'.'):
+                del text[0]
         content = original_content = EMPTYBYTES.join(data)
+        # Discard data immediately to prevent memory pressure
+        data *= 0
         if self._decode_data:
             if self.enable_SMTPUTF8:
-                content = original_content.decode(
-                    'utf-8', errors='surrogateescape')
+                content = original_content.decode('utf-8', errors='surrogateescape')
             else:
                 try:
                     content = original_content.decode('ascii', errors='strict')
@@ -685,6 +739,7 @@ class SMTP(asyncio.StreamReaderProtocol)
                     return
         self.envelope.content = content
         self.envelope.original_content = original_content
+
         # Call the new API first if it's implemented.
         if hasattr(self.event_handler, 'handle_DATA'):
             status = await self._call_handler_hook('DATA')
@@ -694,6 +749,7 @@ class SMTP(asyncio.StreamReaderProtocol)
             if hasattr(self.event_handler, 'process_message'):
                 warn('Use handler.handle_DATA() instead of .process_message()',
                      DeprecationWarning)
+                assert self.session is not None
                 args = (self.session.peer, self.envelope.mail_from,
                         self.envelope.rcpt_tos, self.envelope.content)
                 if asyncio.iscoroutinefunction(
Index: aiosmtpd-1.2.1/aiosmtpd/tests/test_smtpsmuggling.py
===================================================================
--- /dev/null
+++ aiosmtpd-1.2.1/aiosmtpd/tests/test_smtpsmuggling.py
@@ -0,0 +1,134 @@
+# Copyright 2014-2021 The aiosmtpd Developers
+# SPDX-License-Identifier: Apache-2.0
+
+"""Test SMTP smuggling."""
+
+from email.mime.text import MIMEText
+from smtplib import SMTP, SMTP_SSL
+from typing import Generator, Union
+
+import re
+import socket
+import pytest
+import unittest
+import smtplib
+
+from aiosmtpd.controller import Controller
+
+from aiosmtpd.smtp import SMTP as Server
+from aiosmtpd.smtp import Session as ServerSession
+from aiosmtpd.smtp import Envelope
+
+
+class ReceivingHandler:
+    box = None
+
+    def __init__(self):
+        self.box = []
+
+    async def handle_DATA(self, server, session, envelope):
+        self.box.append(envelope)
+        return '250 OK'
+
+
+def new_data(self, msg):
+        self.putcmd("data")
+
+        (code, repl) = self.getreply()
+        if self.debuglevel > 0:
+            self._print_debug('data:', (code, repl))
+        if code != 354:
+            raise SMTPDataError(code, repl)
+        else:
+            ##### Patching input encoding so we can send raw messages
+            #if isinstance(msg, str):
+            #    msg = smtplib._fix_eols(msg).encode('ascii')
+            #q = smtplib._quote_periods(msg)
+            #if q[-2:] != smtplib.bCRLF:
+            #    q = q + smtplib.bCRLF
+            #q = q + b"." + smtplib.bCRLF
+            q = msg
+            self.send(q)
+            (code, msg) = self.getreply()
+            if self.debuglevel > 0:
+                self._print_debug('data:', (code, msg))
+            return (code, msg)
+
+
+def return_unchanged(data):
+    return data
+
+
+def orig_data(self, msg):
+    self.putcmd("data")
+    (code, repl) = self.getreply()
+    if self.debuglevel > 0:
+        self._print_debug('data:', (code, repl))
+    if code != 354:
+        raise smtplib.SMTPDataError(code, repl)
+    else:
+        if isinstance(msg, str):
+            msg = _fix_eols(msg).encode('ascii')
+        q = _quote_periods(msg)
+        if q[-2:] != smtplib.bCRLF:
+            q = q + smtplib.bCRLF
+
+        q = q + b"." + smtplib.bCRLF
+        self.send(q)
+        (code, msg) = self.getreply()
+        if self.debuglevel > 0:
+            self._print_debug('data:', (code, msg))
+        return (code, msg)
+
+
+def _fix_eols(data):
+    return re.sub(r'(?:\r\n|\n|\r(?!\n))', smtplib.CRLF, data)
+
+
+def _quote_periods(bindata):
+    return re.sub(br'(?m)^\.', b'..', bindata)
+
+
+def return_unchanged(data):
+    return data
+
+
+class TestSmuggling(unittest.TestCase):
+    def setUp(self):
+        self.handler = ReceivingHandler()
+        controller = Controller(self.handler)
+        controller.start()
+        self.addCleanup(controller.stop)
+        self.address = (controller.hostname, controller.port)
+
+    def test_smtp_smuggling(self):
+        smtplib._fix_eols = return_unchanged
+        smtplib._quote_periods = return_unchanged
+        smtplib.SMTP.data = new_data
+
+        with SMTP(*self.address) as client:
+            sender = "sender@example.com"
+            recipients = ["rcpt1@example.com"]
+            resp = client.helo("example.com")
+            # S250_FQDN
+            assert resp == (250, bytes(socket.getfqdn(), "utf-8"))
+
+            # Trying SMTP smuggling with a fake \n.\r\n end-of-data sequence.
+            message_data = b"""\
+    From: Anne Person <anne@example.com>\r\n\
+    To: Bart Person <bart@example.com>\r\n\
+    Subject: A test\r\n\
+    Message-ID: <ant>\r\n\
+    \r\n\
+    Testing\
+    \n.\r\n\
+    NO SMUGGLING
+    \r\n.\r\n\
+    """
+            results = client.sendmail(sender, recipients, message_data)
+            client.quit()
+            smtplib._fix_eols = _fix_eols
+            smtplib._quote_periods = _quote_periods
+            smtplib.SMTP.data = orig_data
+
+            assert b"NO SMUGGLING" in self.handler.box[0].content
Index: aiosmtpd-1.2.1/aiosmtpd/tests/test_handlers.py
===================================================================
--- aiosmtpd-1.2.1.orig/aiosmtpd/tests/test_handlers.py
+++ aiosmtpd-1.2.1/aiosmtpd/tests/test_handlers.py
@@ -425,7 +425,7 @@ Testing
             'Subject: A test',
             'X-Peer: ::1',
             '',
-            'Testing']).encode('ascii')
+            'Testing', '']).encode('ascii')
 
     def test_deliver_bytes(self):
         with ExitStack() as resources:
Index: aiosmtpd-1.2.1/aiosmtpd/tests/test_smtp.py
===================================================================
--- aiosmtpd-1.2.1.orig/aiosmtpd/tests/test_smtp.py
+++ aiosmtpd-1.2.1/aiosmtpd/tests/test_smtp.py
@@ -159,14 +159,14 @@ class TestProtocol(unittest.TestCase):
 
     def test_honors_mail_delimeters(self):
         handler = ReceivingHandler()
-        data = b'test\r\nmail\rdelimeters\nsaved'
+        data = b'test\r\nmail\rdelimeters\nsaved\r\n'
         protocol = self._get_protocol(handler)
         protocol.data_received(BCRLF.join([
             b'HELO example.org',
             b'MAIL FROM: <anne@example.com>',
             b'RCPT TO: <anne@example.com>',
             b'DATA',
-            data + b'\r\n.',
+            data + b'.',
             b'QUIT\r\n'
             ]))
         try:
@@ -902,7 +902,8 @@ Testing
             mail = CRLF.join(['Test', '.', 'mail'])
             client.sendmail('anne@example.com', ['bart@example.com'], mail)
             self.assertEqual(len(handler.box), 1)
-            self.assertEqual(handler.box[0].content, 'Test\r\n.\r\nmail')
+            self.assertEqual(handler.box[0].content, mail + CRLF)
+
 
     def test_unexpected_errors(self):
         handler = ErroringHandler()
openSUSE Build Service is sponsored by