티스토리 수익 글 보기

티스토리 수익 글 보기

[6.1.x] Fixed CVE-2026-7666 — Delayed setting SMTP connection until … · django/django@afd82a5 · GitHub
Skip to content

Commit afd82a5

Browse files
RealOrangeOnenessita
authored andcommitted
[6.1.x] Fixed CVE-2026-7666 — Delayed setting SMTP connection until fully configured.
Thanks Kasper Dupont for the report, and Jacob Walls and Natalia Bidart for reviews. Backport of df887f5 from main.
1 parent 42bdfd7 commit afd82a5

4 files changed

Lines changed: 97 additions & 27 deletions

File tree

django/core/mail/backends/smtp.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def __init__(
4545
super().__init__(**kwargs)
4646
self.fail_silently = fail_silently
4747
self.connection = None
48+
self._partial_connection = None
4849
self._lock = threading.RLock()
4950

5051
# RemovedInDjango70Warning.
@@ -120,6 +121,11 @@ def open(self):
120121
# Nothing to do if the connection is already open.
121122
return False
122123

124+
# If a connection was partially opened before, close it.
125+
if self._partial_connection is not None:
126+
self._close_connection(self._partial_connection)
127+
self._partial_connection = None
128+
123129
# If local_hostname is not specified, socket.getfqdn() gets used.
124130
# For performance, we use the cached FQDN for local_hostname.
125131
connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
@@ -128,39 +134,51 @@ def open(self):
128134
if self.use_ssl:
129135
connection_params["context"] = self.ssl_context
130136
try:
131-
self.connection = self.connection_class(
137+
self._partial_connection = self.connection_class(
132138
self.host, self.port, **connection_params
133139
)
134140

135141
# TLS/SSL are mutually exclusive, so only attempt TLS over
136142
# non-secure connections.
137143
if not self.use_ssl and self.use_tls:
138-
self.connection.starttls(context=self.ssl_context)
144+
self._partial_connection.starttls(context=self.ssl_context)
139145
if self.username and self.password:
140-
self.connection.login(self.username, self.password)
146+
self._partial_connection.login(self.username, self.password)
147+
148+
# Don't set connection until it's fully configured.
149+
self.connection = self._partial_connection
150+
self._partial_connection = None
151+
141152
return True
142153
except OSError:
143154
if not self.fail_silently:
144155
raise
145156

157+
def _close_connection(self, connection):
158+
try:
159+
connection.quit()
160+
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
161+
# This happens when calling quit() on a TLS connection
162+
# sometimes, or when the connection was already disconnected
163+
# by the server.
164+
connection.close()
165+
except smtplib.SMTPException:
166+
if self.fail_silently:
167+
return
168+
raise
169+
146170
def close(self):
147171
"""Close the connection to the email server."""
148-
if self.connection is None:
149-
return
150-
try:
172+
if self._partial_connection is not None:
151173
try:
152-
self.connection.quit()
153-
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
154-
# This happens when calling quit() on a TLS connection
155-
# sometimes, or when the connection was already disconnected
156-
# by the server.
157-
self.connection.close()
158-
except smtplib.SMTPException:
159-
if self.fail_silently:
160-
return
161-
raise
162-
finally:
163-
self.connection = None
174+
self._close_connection(self._partial_connection)
175+
finally:
176+
self._partial_connection = None
177+
if self.connection is not None:
178+
try:
179+
self._close_connection(self.connection)
180+
finally:
181+
self.connection = None
164182

165183
def send_messages(self, email_messages):
166184
"""

docs/releases/5.2.15.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,16 @@ cookies immediately.
2222

2323
This issue has severity "low" according to the :ref:`Django security policy
2424
<severity-levels>`.
25+
26+
CVE-2026-7666: Potential unencrypted email transmission via ``STARTTLS`` in the SMTP backend
27+
============================================================================================
28+
29+
When using :setting:`EMAIL_USE_TLS`, a failed ``STARTTLS`` handshake could
30+
leave a partially-initialized connection that would subsequently be reused for
31+
sending email without encryption. This can occur with ``fail_silently=True``,
32+
as used by :func:`~django.core.mail.send_mail` and
33+
:class:`~django.middleware.common.BrokenLinkEmailsMiddleware`, among others.
34+
Connections configured with :setting:`EMAIL_USE_SSL` are not affected.
35+
36+
This issue has severity "low" according to the :ref:`Django security policy
37+
<severity-levels>`.

docs/releases/6.0.6.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ cookies immediately.
2424
This issue has severity "low" according to the :ref:`Django security policy
2525
<severity-levels>`.
2626

27+
CVE-2026-7666: Potential unencrypted email transmission via ``STARTTLS`` in the SMTP backend
28+
============================================================================================
29+
30+
When using :setting:`EMAIL_USE_TLS`, a failed ``STARTTLS`` handshake could
31+
leave a partially-initialized connection that would subsequently be reused for
32+
sending email without encryption. This can occur with ``fail_silently=True``,
33+
as used by :func:`~django.core.mail.send_mail` and
34+
:class:`~django.middleware.common.BrokenLinkEmailsMiddleware`, among others.
35+
Connections configured with :setting:`EMAIL_USE_SSL` are not affected.
36+
37+
This issue has severity "low" according to the :ref:`Django security policy
38+
<severity-levels>`.
39+
2740
Bugfixes
2841
========
2942

tests/mail/test_backends.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -795,10 +795,9 @@ def test_auth_attempted(self):
795795
backend = self.create_backend(
796796
username="not empty username", password="not empty password"
797797
)
798-
with mock.patch("smtplib.SMTP.login") as mock_smtp_login, backend:
799-
# Using backend as context manager opens the connection and
800-
# attempts login.
801-
pass
798+
self.addCleanup(backend.close)
799+
with mock.patch("smtplib.SMTP.login") as mock_smtp_login:
800+
backend.open()
802801
mock_smtp_login.assert_called_once_with(
803802
"not empty username", "not empty password"
804803
)
@@ -810,15 +809,39 @@ def test_server_open(self):
810809
backend = self.create_backend()
811810
self.assertIsNone(backend.connection)
812811
opened = backend.open()
812+
self.assertIsNotNone(backend.connection)
813+
self.assertIsNone(backend._partial_connection)
813814
backend.close()
814815
self.assertIs(opened, True)
816+
self.assertIsNone(backend.connection)
817+
self.assertIsNone(backend._partial_connection)
815818

816819
def test_reopen_connection(self):
817820
backend = self.create_backend()
818821
# Simulate an already open connection.
819822
backend.connection = mock.Mock(spec=object())
820823
self.assertIs(backend.open(), False)
821824

825+
def test_reopen_replaces_partial_connection(self):
826+
backend = self.create_backend(username="not empty", password="not empty")
827+
self.addCleanup(backend.close)
828+
829+
error = "SMTP AUTH extension not supported by server."
830+
with self.assertRaisesMessage(SMTPException, error):
831+
backend.open()
832+
self.assertIsNone(backend.connection)
833+
self.assertIsNotNone(backend._partial_connection)
834+
partial_conn = backend._partial_connection
835+
836+
with self.assertRaisesMessage(SMTPException, error):
837+
backend.open()
838+
self.assertIsNone(backend.connection)
839+
self.assertIsNotNone(backend._partial_connection)
840+
self.assertNotEqual(backend._partial_connection, partial_conn)
841+
842+
self.assertIsNone(partial_conn.sock)
843+
self.assertIsNotNone(backend._partial_connection.sock)
844+
822845
# RemovedInDjango70Warning.
823846
@override_settings(EMAIL_USE_TLS=True)
824847
def test_email_tls_use_settings(self):
@@ -915,19 +938,20 @@ def test_ssl_context_uses_ssl_certfile_and_keyfile(self):
915938

916939
def test_email_tls_attempts_starttls(self):
917940
backend = self.create_backend(use_tls=True)
941+
self.addCleanup(backend.close)
918942
self.assertIs(backend.use_tls, True)
919943
with self.assertRaisesMessage(
920944
SMTPException, "STARTTLS extension not supported by server."
921945
):
922-
with backend:
923-
pass
946+
backend.open()
947+
self.assertIsNone(backend.connection)
924948

925949
def test_email_ssl_attempts_ssl_connection(self):
926950
backend = self.create_backend(use_ssl=True)
927951
self.assertIs(backend.use_ssl, True)
928952
with self.assertRaises(SSLError):
929-
with backend:
930-
pass
953+
backend.open()
954+
self.assertIsNone(backend.connection)
931955

932956
def test_connection_timeout_default(self):
933957
backend = self.create_backend()
@@ -944,10 +968,10 @@ def __init__(self, *args, **kwargs):
944968
myemailbackend = MyEmailBackend(
945969
host=self.smtp_controller.hostname, port=self.smtp_controller.port
946970
)
971+
self.addCleanup(myemailbackend.close)
947972
myemailbackend.open()
948973
self.assertEqual(myemailbackend.timeout, 42)
949974
self.assertEqual(myemailbackend.connection.timeout, 42)
950-
myemailbackend.close()
951975

952976
# RemovedInDjango70Warning.
953977
@override_settings(EMAIL_TIMEOUT=10)
@@ -1158,5 +1182,7 @@ def test_fail_silently_on_connection_error(self):
11581182
"""
11591183
with self.assertRaises(ConnectionError):
11601184
self.backend.open()
1185+
self.assertIsNone(self.backend.connection)
11611186
self.backend.fail_silently = True
11621187
self.backend.open()
1188+
self.assertIsNone(self.backend.connection)

0 commit comments

Comments
 (0)