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 = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to discard all cached state that could have been received before a STARTTLS command, so that includes untagged_responses, too. We should add a test for this case.

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