Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Lib/email/_header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2641,7 +2641,7 @@ def _refold_parse_tree(parse_tree, *, policy):
want_encoding = False
last_ew = None
if part.syntactic_break:
encoded_part = part.fold(policy=policy)[:-1] # strip nl
encoded_part = part.fold(policy=policy)[:-len(policy.linesep)]
if policy.linesep not in encoded_part:
# It fits on a single line
if len(encoded_part) > maxlen - len(lines[-1]):
Expand Down
15 changes: 14 additions & 1 deletion Lib/email/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NLCRE = re.compile(r'\r\n|\r|\n')
fcre = re.compile(r'^From ', re.MULTILINE)
NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')



Expand Down Expand Up @@ -429,7 +430,19 @@ def _write_headers(self, msg):
# This is almost the same as the string version, except for handling
# strings with 8bit bytes.
for h, v in msg.raw_items():
self._fp.write(self.policy.fold_binary(h, v))
folded = self.policy.fold_binary(h, v)
if self.policy.verify_generated_headers:
linesep = self.policy.linesep.encode()
if not folded.endswith(linesep):
raise HeaderWriteError(
f'folded header does not end with {linesep!r}: {folded!r}')
folded_no_linesep = folded
if folded.endswith(linesep):
folded_no_linesep = folded[:-len(linesep)]
if NEWLINE_WITHOUT_FWSP_BYTES.search(folded_no_linesep):
raise HeaderWriteError(
f'folded header contains newline: {folded!r}')
self._fp.write(folded)
# A blank line always separates headers from body
self.write(self._NL)

Expand Down
4 changes: 3 additions & 1 deletion Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'

_control_chars = re.compile(b'[\x00-\x1F\x7F]')


class IMAP4:
Expand Down Expand Up @@ -958,6 +958,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("Control characters not allowed in commands")
data = data + b' ' + arg

literal = self.literal
Expand Down
2 changes: 2 additions & 0 deletions Lib/poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ def _putline(self, line):
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
line = bytes(line, self.encoding)
if re.search(b'[\x00-\x1F\x7F]', line):
raise ValueError('Control characters not allowed in commands')
self._putline(line)


Expand Down
7 changes: 7 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2902,3 +2902,10 @@ def adjust_int_max_str_digits(max_digits):
yield
finally:
sys.set_int_max_str_digits(current)


def control_characters_c0():
"""Returns a list of C0 control characters as strings.
C0 control characters defined as the byte range 0x00-0x1F, and 0x7F.
"""
return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"]
26 changes: 25 additions & 1 deletion Lib/test/test_email/test_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from email import message_from_string, message_from_bytes
from email.message import EmailMessage
from email.generator import Generator, BytesGenerator
from email.headerregistry import Address
from email import policy
import email.errors
from test.test_email import TestEmailBase, parameterize
Expand Down Expand Up @@ -263,7 +264,7 @@ class TestGenerator(TestGeneratorBase, TestEmailBase):
typ = str

def test_verify_generated_headers(self):
"""gh-121650: by default the generator prevents header injection"""
# gh-121650: by default the generator prevents header injection
class LiteralHeader(str):
name = 'Header'
def fold(self, **kwargs):
Expand All @@ -284,6 +285,8 @@ def fold(self, **kwargs):

with self.assertRaises(email.errors.HeaderWriteError):
message.as_string()
with self.assertRaises(email.errors.HeaderWriteError):
message.as_bytes()


class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
Expand Down Expand Up @@ -353,6 +356,27 @@ def test_smtputf8_policy(self):
g.flatten(msg)
self.assertEqual(s.getvalue(), expected)

def test_smtp_policy(self):
msg = EmailMessage()
msg["From"] = Address(addr_spec="foo@bar.com", display_name="Páolo")
msg["To"] = Address(addr_spec="bar@foo.com", display_name="Dinsdale")
msg["Subject"] = "Nudge nudge, wink, wink"
msg.set_content("oh boy, know what I mean, know what I mean?")
expected = textwrap.dedent("""\
From: =?utf-8?q?P=C3=A1olo?= <foo@bar.com>
To: Dinsdale <bar@foo.com>
Subject: Nudge nudge, wink, wink
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0

oh boy, know what I mean, know what I mean?
""").encode().replace(b"\n", b"\r\n")
s = io.BytesIO()
g = BytesGenerator(s, policy=policy.SMTP)
g.flatten(msg)
self.assertEqual(s.getvalue(), expected)


if __name__ == '__main__':
unittest.main()
6 changes: 5 additions & 1 deletion Lib/test/test_email/test_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def test_short_maxlen_error(self):
policy.fold("Subject", subject)

def test_verify_generated_headers(self):
"""Turning protection off allows header injection"""
# Turning protection off allows header injection
policy = email.policy.default.clone(verify_generated_headers=False)
for text in (
'Header: Value\r\nBad: Injection\r\n',
Expand All @@ -290,6 +290,10 @@ def fold(self, **kwargs):
message.as_string(),
f"{text}\nBody",
)
self.assertEqual(
message.as_bytes(),
f"{text}\nBody".encode(),
)

# XXX: Need subclassing tests.
# For adding subclassed objects, make sure the usual rules apply (subclass
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,12 @@ def test_login(self):
self.assertEqual(data[0], b'LOGIN completed')
self.assertEqual(client.state, 'AUTH')

def test_control_characters(self):
client, _ = self._setup(SimpleIMAPHandler)
for c0 in support.control_characters_c0():
with self.assertRaises(ValueError):
client.login(f'user{c0}', 'pass')

def test_logout(self):
client, _ = self._setup(SimpleIMAPHandler)
typ, data = client.login('user', 'pass')
Expand Down
8 changes: 8 additions & 0 deletions Lib/test/test_poplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from unittest import TestCase, skipUnless
from test import support as test_support
from test.support import control_characters_c0
threading = test_support.import_module('threading')

HOST = test_support.HOST
Expand Down Expand Up @@ -349,6 +350,13 @@ def test_quit(self):
self.assertIsNone(self.client.sock)
self.assertIsNone(self.client.file)

def test_control_characters(self):
for c0 in control_characters_c0():
with self.assertRaises(ValueError):
self.client.user(f'user{c0}')
with self.assertRaises(ValueError):
self.client.pass_(f'{c0}pass')

@requires_ssl
def test_stls_capa(self):
capa = self.client.capa()
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_wsgiref.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from unittest import mock
from test import support
from test.support import control_characters_c0
from test.test_httpservers import NoLogRequestHandler
from unittest import TestCase
from wsgiref.util import setup_testing_defaults
Expand Down Expand Up @@ -517,6 +518,16 @@ def testExtras(self):
'\r\n'
)

def testRaisesControlCharacters(self):
headers = Headers()
for c0 in control_characters_c0():
self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val")
self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}")
self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param")
self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param")
self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}")


class ErrorHandler(BaseCGIHandler):
"""Simple handler subclass for testing BaseHandler"""

Expand Down
3 changes: 3 additions & 0 deletions Lib/wsgiref/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# existence of which force quoting of the parameter value.
import re
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
_control_chars_re = re.compile(r'[\x00-\x1F\x7F]')

def _formatparam(param, value=None, quote=1):
"""Convenience function to format and return a key=value pair.
Expand Down Expand Up @@ -41,6 +42,8 @@ def __init__(self, headers=None):
def _convert_string_type(self, value):
"""Convert/check value type."""
if type(value) is str:
if _control_chars_re.search(value):
raise ValueError("Control characters not allowed in headers")
return value
raise AssertionError("Header names/values must be"
" of type str (got {0})".format(repr(value)))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix serialization of messages containing encoded strings when the
policy.linesep is set to a multi-character string. Patch by Jens Troeger.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Reject C0 control characters within wsgiref.headers.Headers fields, values,
and parameters.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in IMAP commands.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject control characters in POP3 commands.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers
that are unsafely folded or delimited; see
:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
Bloemsaat and Petr Viktorin in :gh:`121650`).