From 3255f64a38d704823ae2d76058a61a3ed2df8677 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 19:06:33 +0300 Subject: [PATCH] [3.14] gh-143921: Reject NUL, CR and LF in IMAP commands (GH-143922, GH-153067) Combined backport of GH-143922, which rejected all control characters, and GH-153067, which narrowed the check to NUL, CR and LF. Other control characters are valid in quoted strings and are sent quoted. (cherry picked from commit 6262704b134db2a4ba12e85ecfbd968534f28b45) (cherry picked from commit d0921efb665aff26b378f495e5ff84f7e3fe649d) Co-authored-by: Seth Michael Larson Co-Authored-By: Claude Fable 5 --- Lib/imaplib.py | 5 +++++ Lib/test/test_imaplib.py | 16 ++++++++++++++++ ...026-01-16-11-41-06.gh-issue-143921.AeCOor.rst | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 4f5e4fc5b9e569b..ac443ac915ce4fc 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -131,6 +131,9 @@ # We compile these in _mode_xxx. _Literal = br'.*{(?P\d+)}$' _Untagged_status = br'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?' +# Only NUL, CR and LF are unsafe (they cannot be represented even in +# a quoted string); other control characters are sent quoted. +_control_chars = re.compile(b'[\x00\r\n]') _non_astring_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff%*\\"]') _non_list_char = re.compile(br'[(){ \x00-\x1f\x7f-\xff\\"]') _quoted = re.compile(br'"(?:[^"\\]|\\.)*+"') @@ -1155,6 +1158,8 @@ def _command(self, name, *args): if arg is None: continue if isinstance(arg, str): arg = bytes(arg, self._encoding) + if _control_chars.search(arg): + raise ValueError("NUL, CR and LF not allowed in commands") data = data + b' ' + arg literal = self.literal diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 4e44666800e7935..8a71b6f89393865 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -1741,6 +1741,22 @@ def test_uppercase_command_names(self): with self.assertRaises(AttributeError): client.NONEXISTENT + def test_control_characters(self): + client, server = self._setup(SimpleIMAPHandler) + client.login('user', 'pass') + for c in '\0\r\n': + with self.assertRaises(ValueError): + client.select(f'a{c}b') + # Other control characters are valid in a quoted string and can + # occur in mailbox names returned by the server, so the client + # must be able to send them back. + for c in support.control_characters_c0(): + if c in '\0\r\n': + continue + typ, _ = client.select(f'a{c}b') + self.assertEqual(typ, 'OK') + self.assertEqual(server.is_selected, [f'"a{c}b"']) + # property tests def test_file_property_should_not_be_accessed(self): diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst new file mode 100644 index 000000000000000..de62e020a95e6b2 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst @@ -0,0 +1,2 @@ +Reject NUL, CR and LF characters in IMAP commands. Other control +characters are allowed and sent quoted.