Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,9 @@ def starttls(self, ssl_context=None):
ssl_context = ssl._create_stdlib_context()
typ, dat = self._simple_command(name)
if typ == 'OK':
if any(self._readbuf):
raise self.abort('unexpected data after STARTTLS response')
self._readbuf = []
self._close_imaplib_file()
self.sock = ssl_context.wrap_socket(self.sock,
server_hostname=self.host)
Expand Down
61 changes: 61 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,33 @@ def test_substitute_errors(self):
self.assertRaises(ValueError, sub, '?s', [[(1,)]]) # not a range pair
self.assertRaises(ValueError, sub, '?s', [[(1, 2, 3)]]) # not a range pair

@unittest.skipUnless(ssl, "SSL not available")
def test_starttls_rejects_buffered_plaintext(self):
sock = mock.Mock(spec=socket.socket)
sock.recv.return_value = (
b'* OK [CAPABILITY IMAP4rev1 STARTTLS] ready\r\n')
with mock.patch.object(imaplib.IMAP4, '_create_socket',
return_value=sock):
client = imaplib.IMAP4('localhost')
self.addCleanup(client.shutdown)

tag = client.tagpre + str(client.tagnum).encode('ascii')
next_tag = client.tagpre + str(client.tagnum + 1).encode('ascii')
# Return the STARTTLS response and injected plaintext in one recv(),
# so the test does not depend on how TCP packets are split.
sock.recv.side_effect = [
tag + b' OK Begin TLS\r\n'
b'* CAPABILITY IMAP4rev1 INJECTED\r\n'
+ next_tag + b' OK CAPABILITY completed\r\n'
]
context = mock.Mock()
context.wrap_socket.return_value = sock
with self.assertRaisesRegex(imaplib.IMAP4.abort,
'unexpected data after STARTTLS response'):
client.starttls(context)
context.wrap_socket.assert_not_called()
self.assertFalse(client._tls_established)


if ssl:
class SecureTCPServer(socketserver.TCPServer):
Expand Down Expand Up @@ -2349,6 +2376,40 @@ class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
imap_class = imaplib.IMAP4
server_class = socketserver.TCPServer

@unittest.skipUnless(ssl, "SSL not available")
def test_starttls(self):
class StartTLSHandler(SimpleIMAPHandler):
capabilities = 'STARTTLS'

def cmd_STARTTLS(self, tag, args):
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(CERTFILE)
self._send_tagged(tag, 'OK', 'Begin TLS')
self.connection = context.wrap_socket(self.connection,
server_side=True)
self.rfile.close()
self.wfile.close()
self.rfile = self.connection.makefile('rb')
self.wfile = self.connection.makefile('wb', buffering=0)
self.capabilities = 'AUTH=PLAIN'

def finish(self):
try:
super().finish()
finally:
self.connection.close()

_, server = self._setup(StartTLSHandler, connect=False)
self.client = client = self.imap_class(
'localhost', server.server_address[1],
timeout=support.LOOPBACK_TIMEOUT)
context = ssl.create_default_context(cafile=CAFILE)
self.assertEqual(client.capabilities, ('IMAP4REV1', 'STARTTLS'))
self.assertEqual(client.starttls(context), ('OK', [None]))
self.assertIsInstance(client.sock, ssl.SSLSocket)
self.assertEqual(client.capabilities, ('IMAP4REV1', 'AUTH=PLAIN'))
client.logout()


@unittest.skipUnless(ssl, "SSL not available")
class NewIMAPSSLTests(NewIMAPTestsMixin, unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.. cve: CVE-2026-115559

:meth:`imaplib.IMAP4.starttls` now rejects unexpected plaintext buffered after
the STARTTLS response, preventing it from being processed as TLS-protected data.
Loading