https://github.com/python/cpython/commit/766614f88af0433c2c14f5c4ed11c92d0fb04e61
commit: 766614f88af0433c2c14f5c4ed11c92d0fb04e61
branch: main
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2025-08-22T11:45:01Z
summary:
gh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (#136623)
files:
A Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst
M Lib/smtplib.py
M Lib/test/test_smtplib.py
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index 84d6d858e7dec1..b71fee8777e866 100644
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -177,6 +177,15 @@ def _quote_periods(bindata):
def _fix_eols(data):
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
+
+try:
+ hmac.digest(b'', b'', 'md5')
+except ValueError:
+ _have_cram_md5_support = False
+else:
+ _have_cram_md5_support = True
+
+
try:
import ssl
except ImportError:
@@ -665,8 +674,11 @@ def auth_cram_md5(self, challenge=None):
# CRAM-MD5 does not support initial-response.
if challenge is None:
return None
- return self.user + " " + hmac.HMAC(
- self.password.encode('ascii'), challenge, 'md5').hexdigest()
+ if not _have_cram_md5_support:
+ raise SMTPException("CRAM-MD5 is not supported")
+ password = self.password.encode('ascii')
+ authcode = hmac.HMAC(password, challenge, 'md5')
+ return f"{self.user} {authcode.hexdigest()}"
def auth_plain(self, challenge=None):
""" Authobject to use with PLAIN authentication. Requires self.user and
@@ -718,9 +730,10 @@ def login(self, user, password, *,
initial_response_ok=True):
advertised_authlist = self.esmtp_features["auth"].split()
# Authentication methods we can handle in our preferred order:
- preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
-
- # We try the supported authentications in our preferred order, if
+ if _have_cram_md5_support:
+ preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
+ else:
+ preferred_auths = ['PLAIN', 'LOGIN']
# the server supports them.
authlist = [auth for auth in preferred_auths
if auth in advertised_authlist]
diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
index 4c9fc14bd43f54..b8aac8c20202a2 100644
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -17,6 +17,7 @@
import threading
import unittest
+import unittest.mock as mock
from test import support, mock_socket
from test.support import hashlib_helper
from test.support import socket_helper
@@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None):
except ValueError as e:
self.push('535 Splitting response {!r} into user and password '
'failed: {}'.format(logpass, e))
- return False
- valid_hashed_pass = hmac.HMAC(
- sim_auth[1].encode('ascii'),
- self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
- 'md5').hexdigest()
+ return
+ pwd = sim_auth[1].encode('ascii')
+ msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii')
+ try:
+ valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest()
+ except ValueError:
+ self.push('504 CRAM-MD5 is not supported')
+ return
self._authenticated(user, hashed_pass == valid_hashed_pass)
# end AUTH related stuff.
@@ -1181,6 +1185,39 @@ def testAUTH_CRAM_MD5(self):
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
+ @hashlib_helper.block_algorithm('md5')
+ @mock.patch("smtplib._have_cram_md5_support", False)
+ def testAUTH_CRAM_MD5_blocked(self):
+ # CRAM-MD5 is the only "known" method by the server,
+ # but it is not supported by the client. In particular,
+ # no challenge will ever be sent.
+ self.serv.add_feature("AUTH CRAM-MD5")
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=support.LOOPBACK_TIMEOUT)
+ self.addCleanup(smtp.close)
+ msg = re.escape("No suitable authentication method found.")
+ with self.assertRaisesRegex(smtplib.SMTPException, msg):
+ smtp.login(sim_auth[0], sim_auth[1])
+
+ @hashlib_helper.block_algorithm('md5')
+ @mock.patch("smtplib._have_cram_md5_support", False)
+ def testAUTH_CRAM_MD5_blocked_and_fallback(self):
+ # Test that PLAIN is tried after CRAM-MD5 failed
+ self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=support.LOOPBACK_TIMEOUT)
+ self.addCleanup(smtp.close)
+ with (
+ mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
+ mock.patch.object(
+ smtp, "auth_plain", wraps=smtp.auth_plain
+ ) as smtp_auth_plain
+ ):
+ resp = smtp.login(sim_auth[0], sim_auth[1])
+ smtp_auth_plain.assert_called_once()
+ smtp_auth_cram_md5.assert_not_called()
+ self.assertEqual(resp, (235, b'Authentication Succeeded'))
+
@hashlib_helper.requires_hashdigest('md5', openssl=True)
def testAUTH_multiple(self):
# Test that multiple authentication methods are tried.
diff --git
a/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst
b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst
new file mode 100644
index 00000000000000..f0290be9ba1e05
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst
@@ -0,0 +1,5 @@
+:meth:`!SMTP.auth_cram_md5` now raises an :exc:`~smtplib.SMTPException`
+instead of a :exc:`ValueError` if Python has been built without MD5 support.
+In particular, :class:`~smtplib.SMTP` clients will not attempt to use this
+method even if the remote server is assumed to support it. Patch by Bénédikt
+Tran.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]