Skip to content

Commit 8cadc2c

Browse files
orsenthilpepoluan
andauthored
[3.8] bpo-27820: Fix AUTH LOGIN logic in smtplib.SMTP (GH-24118) (#24833)
* bpo-27820: Fix AUTH LOGIN logic in smtplib.SMTP (GH-24118) * Fix auth_login logic (bpo-27820) * Also fix a longstanding bug in the SimSMTPChannel.found_terminator() method that causes inability to test SMTP AUTH with initial_response_ok=False. (cherry picked from commit 7591d94) * Set timeout to 15 directly. Co-authored-by: Pandu E POLUAN <[email protected]>
1 parent 1a5001c commit 8cadc2c

File tree

3 files changed

+60
-3
lines changed

3 files changed

+60
-3
lines changed

Lib/smtplib.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
CRLF = "\r\n"
6565
bCRLF = b"\r\n"
6666
_MAXLINE = 8192 # more than 8 times larger than RFC 821, 4.5.3
67+
_MAXCHALLENGE = 5 # Maximum number of AUTH challenges sent
6768

6869
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
6970

@@ -248,6 +249,7 @@ def __init__(self, host='', port=0, local_hostname=None,
248249
self.esmtp_features = {}
249250
self.command_encoding = 'ascii'
250251
self.source_address = source_address
252+
self._auth_challenge_count = 0
251253

252254
if host:
253255
(code, msg) = self.connect(host, port)
@@ -631,14 +633,23 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True):
631633
if initial_response is not None:
632634
response = encode_base64(initial_response.encode('ascii'), eol='')
633635
(code, resp) = self.docmd("AUTH", mechanism + " " + response)
636+
self._auth_challenge_count = 1
634637
else:
635638
(code, resp) = self.docmd("AUTH", mechanism)
639+
self._auth_challenge_count = 0
636640
# If server responds with a challenge, send the response.
637-
if code == 334:
641+
while code == 334:
642+
self._auth_challenge_count += 1
638643
challenge = base64.decodebytes(resp)
639644
response = encode_base64(
640645
authobject(challenge).encode('ascii'), eol='')
641646
(code, resp) = self.docmd(response)
647+
# If server keeps sending challenges, something is wrong.
648+
if self._auth_challenge_count > _MAXCHALLENGE:
649+
raise SMTPException(
650+
"Server AUTH mechanism infinite loop. Last response: "
651+
+ repr((code, resp))
652+
)
642653
if code in (235, 503):
643654
return (code, resp)
644655
raise SMTPAuthenticationError(code, resp)
@@ -660,7 +671,7 @@ def auth_plain(self, challenge=None):
660671
def auth_login(self, challenge=None):
661672
""" Authobject to use with LOGIN authentication. Requires self.user and
662673
self.password to be set."""
663-
if challenge is None:
674+
if challenge is None or self._auth_challenge_count < 2:
664675
return self.user
665676
else:
666677
return self.password

Lib/test/test_smtplib.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ def found_terminator(self):
733733
except ResponseException as e:
734734
self.smtp_state = self.COMMAND
735735
self.push('%s %s' % (e.smtp_code, e.smtp_error))
736-
return
736+
return
737737
super().found_terminator()
738738

739739

@@ -799,6 +799,11 @@ def _auth_login(self, arg=None):
799799
self._authenticated(self._auth_login_user, password == sim_auth[1])
800800
del self._auth_login_user
801801

802+
def _auth_buggy(self, arg=None):
803+
# This AUTH mechanism will 'trap' client in a neverending 334
804+
# base64 encoded 'BuGgYbUgGy'
805+
self.push('334 QnVHZ1liVWdHeQ==')
806+
802807
def _auth_cram_md5(self, arg=None):
803808
if arg is None:
804809
self.push('334 {}'.format(sim_cram_md5_challenge))
@@ -1011,6 +1016,39 @@ def testAUTH_LOGIN(self):
10111016
self.assertEqual(resp, (235, b'Authentication Succeeded'))
10121017
smtp.close()
10131018

1019+
def testAUTH_LOGIN_initial_response_ok(self):
1020+
self.serv.add_feature("AUTH LOGIN")
1021+
with smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) as smtp:
1022+
smtp.user, smtp.password = sim_auth
1023+
smtp.ehlo("test_auth_login")
1024+
resp = smtp.auth("LOGIN", smtp.auth_login, initial_response_ok=True)
1025+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1026+
1027+
def testAUTH_LOGIN_initial_response_notok(self):
1028+
self.serv.add_feature("AUTH LOGIN")
1029+
with smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) as smtp:
1030+
smtp.user, smtp.password = sim_auth
1031+
smtp.ehlo("test_auth_login")
1032+
resp = smtp.auth("LOGIN", smtp.auth_login, initial_response_ok=False)
1033+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1034+
1035+
def testAUTH_BUGGY(self):
1036+
self.serv.add_feature("AUTH BUGGY")
1037+
1038+
def auth_buggy(challenge=None):
1039+
self.assertEqual(b"BuGgYbUgGy", challenge)
1040+
return "\0"
1041+
1042+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
1043+
try:
1044+
smtp.user, smtp.password = sim_auth
1045+
smtp.ehlo("test_auth_buggy")
1046+
expect = r"^Server AUTH mechanism infinite loop.*"
1047+
with self.assertRaisesRegex(smtplib.SMTPException, expect) as cm:
1048+
smtp.auth("BUGGY", auth_buggy, initial_response_ok=False)
1049+
finally:
1050+
smtp.close()
1051+
10141052
@requires_hashdigest('md5')
10151053
def testAUTH_CRAM_MD5(self):
10161054
self.serv.add_feature("AUTH CRAM-MD5")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Fixed long-standing bug of smtplib.SMTP where doing AUTH LOGIN with
2+
initial_response_ok=False will fail.
3+
4+
The cause is that SMTP.auth_login _always_ returns a password if provided
5+
with a challenge string, thus non-compliant with the standard for AUTH
6+
LOGIN.
7+
8+
Also fixes bug with the test for smtpd.

0 commit comments

Comments
 (0)