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
jhaabhijeet864 commentedon Nov 14, 2026
I've reproduced this on main and confirmed the analysis. Root cause is straightforward: IMAP4.append() populates self.literal before _command() is invoked, but the control-character check added in gh-143921 raises while the command line is being built., before _command() reaches the literal-handling path that reads and clears self.literal.
The pending literal then leaks into the next command the client sends, which is both a correctness bug and a data-leak vector: content the caller was told was rejected ends up transmitted to the server anyway.
The same window exists for any failure mode that raises between literal assignment and the literal flush, so I'd like to fix this defensively rather than only patching the ValueError site.
Merge branch 'main' into pythongh-162010
pythongh-162010: Drop pending literal on rejected imaplib.APPEND comm…