Repository navigation
Description
follow-up to gh-143921: the new control character check leaves a rejected APPEND's message pending, and it goes out with the next command
Affected: 71926d9 (3.13, v3.13.15), 2981822
(3.14, v3.14.7) and main (Lib/imaplib.py, IMAP4._command(), as of 2026-11-14). gh-143921 /
CVE-2025-15366.
IMAP4.append() stores the message in self.literal and then calls _command(). The new check raises
ValueError("NUL, CR and LF not allowed in commands") while building the command line, before
_command() reads and clears self.literal. After a rejected APPEND, the message stays pending and
goes out with the next command: the server receives NOOP {7}, then the message bytes, and the
client aborts (command: NOOP => unexpected response: b'message'). It also means data the caller
expected to be rejected gets sent to the server anyway.
Repro (socketpair fake server):
import imaplib, socket, threading, sys
client_sock, server_sock = socket.socketpair()
client_sock.settimeout(5)
seen = []
def serve():
f = server_sock.makefile('rwb')
f.write(b'* PREAUTH ready\r\n'); f.flush()
for line in f:
seen.append(line)
tag = line.split(b' ', 1)[0]
if b'CAPABILITY' in line:
f.write(b'* CAPABILITY IMAP4rev1\r\n')
if line.rstrip().endswith(b'}'):
f.write(b'+ go\r\n'); f.flush(); continue
f.write(tag + b' OK done\r\n'); f.flush()
threading.Thread(target=serve, daemon=True).start()
class Client(imaplib.IMAP4):
def open(self, host='', port=imaplib.IMAP4_PORT, timeout=None):
self.host, self.port, self.sock = host, port, client_sock
self.file = client_sock.makefile('rb')
c = Client()
try:
c.append('bad\nmailbox', None, None, b'message')
except ValueError as e:
print('append rejected:', e)
try:
print('noop ->', c.noop())
except Exception as e:
print('noop raised', type(e).__name__, e)
print('server saw:', [l for l in seen if b'NOOP' in l])
print('literal left:', c.literal)Output on 3.13.15 / main: noop raised abort command: NOOP => unexpected response: b'message',
server saw: [b'XXXX1 NOOP {7}\r\n'].
Suggested fix:
if _control_chars.search(arg):
+ # Drop a pending literal (APPEND) so that it is not sent
+ # with the next command.
+ self.literal = None
raise ValueError("NUL, CR and LF not allowed in commands")plus a test in NewIMAPTestsMixin:
def test_control_characters_append_literal(self):
client, server = self._setup(SimpleIMAPHandler)
client.login('user', 'pass')
with self.assertRaises(ValueError):
client.append('a\nb', None, None, b'message')
self.assertIsNone(client.literal)
typ, _ = client.select('INBOX')
self.assertEqual(typ, 'OK')
Activity