diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst index e0d2c19a3b0737a..624283762f7f08d 100644 --- a/Doc/library/email.utils.rst +++ b/Doc/library/email.utils.rst @@ -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 @@ -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) diff --git a/Lib/email/utils.py b/Lib/email/utils.py index d4824dc3601b2dd..8338669a74c341c 100644 --- a/Lib/email/utils.py +++ b/Lib/email/utils.py @@ -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. @@ -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: diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index d2c2261edbe04e1..90f50247c3360a1 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -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 ' y = '"John X. Doe" ' diff --git a/Misc/NEWS.d/next/Security/2026-05-26-19-30-00.gh-issue-150479.Kq7Lm2.rst b/Misc/NEWS.d/next/Security/2026-05-26-19-30-00.gh-issue-150479.Kq7Lm2.rst new file mode 100644 index 000000000000000..77009cec1d424fa --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-05-26-19-30-00.gh-issue-150479.Kq7Lm2.rst @@ -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`.