Skip to content

imaplib: a rejected APPEND leaves its message pending as the literal of the next command #162010

Open

Description

@omercnet

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directorytopic-emailtype-securityA security issue

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions