Skip to content

Commit 24b6c79

Browse files
Merge pull request from GHSA-pr2m-px7j-xg65
* SMTP Smuggling Fix Adapted adherence to RFC 5321 § 2.3.8 to fix SMTP smuggling issues (https://www.rfc-editor.org/rfc/rfc5321#section-2.3.8) * Apply suggestions from code review Co-authored-by: Sam Bull <git@sambull.org> * Add files via upload * Update test_smtpsmuggling.py --------- Co-authored-by: Sam Bull <git@sambull.org>
1 parent b0267e8 commit 24b6c79

File tree

2 files changed

+85
-5
lines changed

2 files changed

+85
-5
lines changed

aiosmtpd/smtp.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class _DataState(enum.Enum):
8787
EMPTY_BARR = bytearray()
8888
EMPTYBYTES = b''
8989
MISSING = _Missing.MISSING
90-
NEWLINE = '\n'
90+
NEWLINE = '\r\n'
9191
VALID_AUTHMECH = re.compile(r"[A-Z0-9_-]+\Z")
9292

9393
# https://tools.ietf.org/html/rfc3207.html#page-3
@@ -1427,9 +1427,10 @@ async def smtp_DATA(self, arg: str) -> None:
14271427
# Since eof_received cancels this coroutine,
14281428
# readuntil() can never raise asyncio.IncompleteReadError.
14291429
try:
1430-
line: bytes = await self._reader.readuntil()
1430+
# https://datatracker.ietf.org/doc/html/rfc5321#section-2.3.8
1431+
line: bytes = await self._reader.readuntil(b'\r\n')
14311432
log.debug('DATA readline: %s', line)
1432-
assert line.endswith(b'\n')
1433+
assert line.endswith(b'\r\n')
14331434
except asyncio.CancelledError:
14341435
# The connection got reset during the DATA command.
14351436
log.info('Connection lost during DATA')
@@ -1446,7 +1447,7 @@ async def smtp_DATA(self, arg: str) -> None:
14461447
data *= 0
14471448
# Drain the stream anyways
14481449
line = await self._reader.read(e.consumed)
1449-
assert not line.endswith(b'\n')
1450+
assert not line.endswith(b'\r\n')
14501451
# A lone dot in a line signals the end of DATA.
14511452
if not line_fragments and line == b'.\r\n':
14521453
break
@@ -1458,7 +1459,7 @@ async def smtp_DATA(self, arg: str) -> None:
14581459
# Discard data immediately to prevent memory pressure
14591460
data *= 0
14601461
line_fragments.append(line)
1461-
if line.endswith(b'\n'):
1462+
if line.endswith(b'\r\n'):
14621463
# Record data only if state is "NOMINAL"
14631464
if state == _DataState.NOMINAL:
14641465
line = EMPTY_BARR.join(line_fragments)

aiosmtpd/tests/test_smtpsmuggling.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2014-2021 The aiosmtpd Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Test SMTP smuggling."""
5+
6+
from email.mime.text import MIMEText
7+
from smtplib import SMTP, SMTP_SSL
8+
from typing import Generator, Union
9+
10+
import pytest
11+
import smtplib
12+
13+
from aiosmtpd.controller import Controller
14+
from aiosmtpd.testing.helpers import ReceivingHandler
15+
from aiosmtpd.testing.statuscodes import SMTP_STATUS_CODES as S
16+
17+
from aiosmtpd.smtp import SMTP as Server
18+
from aiosmtpd.smtp import Session as ServerSession
19+
from aiosmtpd.smtp import Envelope
20+
21+
from .conftest import Global, controller_data, handler_data
22+
23+
from aiosmtpd.testing.helpers import (
24+
ReceivingHandler
25+
)
26+
27+
def new_data(self, msg):
28+
self.putcmd("data")
29+
30+
(code, repl) = self.getreply()
31+
if self.debuglevel > 0:
32+
self._print_debug('data:', (code, repl))
33+
if code != 354:
34+
raise SMTPDataError(code, repl)
35+
else:
36+
##### Patching input encoding so we can send raw messages
37+
#if isinstance(msg, str):
38+
# msg = smtplib._fix_eols(msg).encode('ascii')
39+
#q = smtplib._quote_periods(msg)
40+
#if q[-2:] != smtplib.bCRLF:
41+
# q = q + smtplib.bCRLF
42+
#q = q + b"." + smtplib.bCRLF
43+
q = msg
44+
self.send(q)
45+
(code, msg) = self.getreply()
46+
if self.debuglevel > 0:
47+
self._print_debug('data:', (code, msg))
48+
return (code, msg)
49+
50+
def return_unchanged(data):
51+
return data
52+
53+
class TestSmuggling:
54+
@handler_data(class_=ReceivingHandler)
55+
def test_smtp_smuggling(self, plain_controller, client):
56+
smtplib._fix_eols = return_unchanged
57+
smtplib._quote_periods = return_unchanged
58+
smtplib.SMTP.data = new_data
59+
60+
handler = plain_controller.handler
61+
sender = "sender@example.com"
62+
recipients = ["rcpt1@example.com"]
63+
resp = client.helo("example.com")
64+
assert resp == S.S250_FQDN
65+
# Trying SMTP smuggling with a fake \n.\r\n end-of-data sequence.
66+
message_data = b"""\
67+
From: Anne Person <anne@example.com>\r\n\
68+
To: Bart Person <bart@example.com>\r\n\
69+
Subject: A test\r\n\
70+
Message-ID: <ant>\r\n\
71+
\r\n\
72+
Testing\
73+
\n.\r\n\
74+
NO SMUGGLING
75+
\r\n.\r\n\
76+
"""
77+
results = client.sendmail(sender, recipients, message_data)
78+
client.quit()
79+
assert b"NO SMUGGLING" in handler.box[0].content

0 commit comments

Comments
 (0)