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
9 changes: 8 additions & 1 deletion Doc/library/email.utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ of the new API.
Add *strict* optional parameter and reject malformed inputs by default.


.. function:: formataddr(pair, charset='utf-8')
.. function:: formataddr(pair, charset='utf-8', *, strict=True)

The inverse of :meth:`parseaddr`, this takes a 2-tuple of the form ``(realname,
email_address)`` and returns the string value suitable for a :mailheader:`To` or
Expand All @@ -82,9 +82,16 @@ of the new API.
characters. Can be an instance of :class:`str` or a
:class:`~email.charset.Charset`. Defaults to ``utf-8``.

If *strict* is true (the default), raise :exc:`ValueError` for inputs that
contain characters invalid in an email address (CR or LF). Set *strict*
to ``False`` to allow non-strict inputs.

.. versionchanged:: 3.3
Added the *charset* option.

.. versionchanged:: 3.16
Added the *strict* parameter.


.. function:: getaddresses(fieldvalues, *, strict=True)

Expand Down
7 changes: 6 additions & 1 deletion Lib/email/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def _sanitize(string):

# Helpers

def formataddr(pair, charset='utf-8'):
def formataddr(pair, charset='utf-8', *, strict=True):
"""The inverse of parseaddr(), this takes a 2-tuple of the form
(realname, email_address) and returns the string value suitable
for an RFC 2822 From, To or Cc header.
Expand All @@ -81,8 +81,13 @@ def formataddr(pair, charset='utf-8'):
realname in case realname is not ASCII safe. Can be an instance of str or
a Charset-like object which has a header_encode method. Default is
'utf-8'.

If strict is True (the default), raise ValueError for inputs that
contain characters invalid in an email address (CR or LF).
"""
name, address = pair
if strict and ('\r' in address or '\n' in address or (name and ('\r' in name or '\n' in name))):
raise ValueError("invalid arguments; address parts cannot contain CR or LF")
# The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
address.encode('ascii')
if name:
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_email/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3277,6 +3277,19 @@ def test_unicode_address_raises_error(self):
self.assertRaises(UnicodeError, utils.formataddr, (None, addr))
self.assertRaises(UnicodeError, utils.formataddr, ("Name", addr))

def test_crlf_in_parts_raises_error(self):
# formataddr() must reject CR and LF in either part so that the
# returned header value cannot be used to inject extra headers,
# matching email.headerregistry.Address.
for name, addr in [
('Real\rName', 'person@dom.ain'),
('Real\nName', 'person@dom.ain'),
('Real Name', 'person@dom.ain\r\nBcc: victim@dom.ain'),
('Real Name', 'person@dom.ain\nSubject: spoofed'),
]:
with self.subTest(name=name, addr=addr):
self.assertRaises(ValueError, utils.formataddr, (name, addr))

def test_name_with_dot(self):
x = 'John X. Doe <jxd@example.com>'
y = '"John X. Doe" <jxd@example.com>'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`email.utils.formataddr` now raises :exc:`ValueError` when the name or
address contains a carriage return or line feed, preventing header injection
and matching :class:`email.headerregistry.Address`.
Loading