From 3ad1fa03ccf0e0e2284cae95b74d22b035bd9ba1 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 15:00:00 -0700 Subject: [PATCH 1/3] gh-160369: Reject buffered plaintext after IMAP STARTTLS --- Lib/imaplib.py | 3 + Lib/test/test_imaplib.py | 61 +++++++++++++++++++ ...10-22-21-59-39.gh-issue-160369.imapTLS.rst | 2 + 3 files changed, 66 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 977d6bdb1c3d86b..76a44016adb2f07 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -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) diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index da63da54e8ef733..b0e61e503fb8139 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -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): @@ -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): diff --git a/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst new file mode 100644 index 000000000000000..5db98f369a3e4e9 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst @@ -0,0 +1,2 @@ +:meth:`imaplib.IMAP4.starttls` now rejects unexpected plaintext buffered after +the STARTTLS response, preventing it from being processed as TLS-protected data. From 0ca2feaac70f63430d9ee02ec809f5d6c301d652 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 15:51:35 -0700 Subject: [PATCH 2/3] gh-160369: Reference CVE-2026-115559 in NEWS --- .../Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst index 5db98f369a3e4e9..07435193cfd7ea9 100644 --- a/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst +++ b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst @@ -1,2 +1,3 @@ :meth:`imaplib.IMAP4.starttls` now rejects unexpected plaintext buffered after the STARTTLS response, preventing it from being processed as TLS-protected data. +This fixes :cve:`2026-115559`. From 46f42ce0835639215497ac5f9923c1295a41cfa0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 15:55:29 -0700 Subject: [PATCH 3/3] gh-160369: Use CVE metadata in NEWS --- .../Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst index 07435193cfd7ea9..35c0a7527457935 100644 --- a/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst +++ b/Misc/NEWS.d/next/Security/2026-10-22-21-59-39.gh-issue-160369.imapTLS.rst @@ -1,3 +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. -This fixes :cve:`2026-115559`.