diff options
author | Florian Apolloner <florian@apolloner.eu> | 2020-07-15 07:30:15 +0200 |
---|---|---|
committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2020-07-20 07:15:14 +0200 |
commit | ccc088f8ced67a9ea57a6ee1e00964f2cf6baffd (patch) | |
tree | 697988edabe55489f8c2bf310f2553b8d62afe44 | |
parent | 5bd7c18306c265dcc03baac4fe0131d5626a6b9f (diff) | |
download | django-ccc088f8ced67a9ea57a6ee1e00964f2cf6baffd.tar.gz |
[3.0.x] Fixed #31784 -- Fixed crash when sending emails on Python 3.6.11+, 3.7.8+, and 3.8.4+.
Fixed sending emails crash on email addresses with display names longer
then 75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+.
Wrapped display names were passed to email.headerregistry.Address()
what caused raising an exception because address parts cannot contain
CR or LF.
See https://bugs.python.org/issue39073
Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Backport of 96a3ea39ef0790dbc413dde0a3e19f6a769356a2 from master
-rw-r--r-- | django/core/mail/message.py | 16 | ||||
-rw-r--r-- | docs/releases/2.2.15.txt | 5 | ||||
-rw-r--r-- | docs/releases/3.0.9.txt | 3 | ||||
-rw-r--r-- | tests/mail/tests.py | 50 |
4 files changed, 63 insertions, 11 deletions
diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 607eb4af0b..963542cd62 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -10,7 +10,7 @@ from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.utils import formatdate, getaddresses, make_msgid +from email.utils import formataddr, formatdate, getaddresses, make_msgid from io import BytesIO, StringIO from pathlib import Path @@ -96,16 +96,24 @@ def sanitize_address(addr, encoding): nm, address = addr localpart, domain = address.rsplit('@', 1) - nm = Header(nm, encoding).encode() + address_parts = nm + localpart + domain + if '\n' in address_parts or '\r' in address_parts: + raise ValueError('Invalid address; address parts cannot contain newlines.') + # Avoid UTF-8 encode, if it's possible. try: + nm.encode('ascii') + nm = Header(nm).encode() + except UnicodeEncodeError: + nm = Header(nm, encoding).encode() + try: localpart.encode('ascii') except UnicodeEncodeError: localpart = Header(localpart, encoding).encode() domain = punycode(domain) - parsed_address = Address(nm, username=localpart, domain=domain) - return str(parsed_address) + parsed_address = Address(username=localpart, domain=domain) + return formataddr((nm, parsed_address.addr_spec)) class MIMEMixin: diff --git a/docs/releases/2.2.15.txt b/docs/releases/2.2.15.txt index df26962029..d8ed58b596 100644 --- a/docs/releases/2.2.15.txt +++ b/docs/releases/2.2.15.txt @@ -4,10 +4,13 @@ Django 2.2.15 release notes *Expected August 3, 2020* -Django 2.2.15 fixes a bug in 2.2.14. +Django 2.2.15 fixes two bugs in 2.2.14. Bugfixes ======== * Allowed setting the ``SameSite`` cookie flag in :meth:`.HttpResponse.delete_cookie` (:ticket:`31790`). + +* Fixed crash when sending emails to addresses with display names longer than + 75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+ (:ticket:`31784`). diff --git a/docs/releases/3.0.9.txt b/docs/releases/3.0.9.txt index 36bcf4546f..65310384ec 100644 --- a/docs/releases/3.0.9.txt +++ b/docs/releases/3.0.9.txt @@ -11,3 +11,6 @@ Bugfixes * Allowed setting the ``SameSite`` cookie flag in :meth:`.HttpResponse.delete_cookie` (:ticket:`31790`). + +* Fixed crash when sending emails to addresses with display names longer than + 75 chars on Python 3.6.11+, 3.7.8+, and 3.8.4+ (:ticket:`31784`). diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 6de819965a..accdba5e4a 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -722,14 +722,14 @@ class MailTests(HeadersCheckMixin, SimpleTestCase): ( ('A name', 'to@example.com'), 'utf-8', - '=?utf-8?q?A_name?= <to@example.com>', + 'A name <to@example.com>', ), ('localpartonly', 'ascii', 'localpartonly'), # ASCII addresses with display names. ('A name <to@example.com>', 'ascii', 'A name <to@example.com>'), - ('A name <to@example.com>', 'utf-8', '=?utf-8?q?A_name?= <to@example.com>'), + ('A name <to@example.com>', 'utf-8', 'A name <to@example.com>'), ('"A name" <to@example.com>', 'ascii', 'A name <to@example.com>'), - ('"A name" <to@example.com>', 'utf-8', '=?utf-8?q?A_name?= <to@example.com>'), + ('"A name" <to@example.com>', 'utf-8', 'A name <to@example.com>'), # Unicode addresses (supported per RFC-6532). ('tó@example.com', 'utf-8', '=?utf-8?b?dMOz?=@example.com'), ('to@éxample.com', 'utf-8', 'to@xn--xample-9ua.com'), @@ -748,20 +748,45 @@ class MailTests(HeadersCheckMixin, SimpleTestCase): ( 'To Example <to@éxample.com>', 'utf-8', - '=?utf-8?q?To_Example?= <to@xn--xample-9ua.com>', + 'To Example <to@xn--xample-9ua.com>', ), # Addresses with two @ signs. ('"to@other.com"@example.com', 'utf-8', r'"to@other.com"@example.com'), ( '"to@other.com" <to@example.com>', 'utf-8', - '=?utf-8?q?to=40other=2Ecom?= <to@example.com>', + '"to@other.com" <to@example.com>', ), ( ('To Example', 'to@other.com@example.com'), 'utf-8', - '=?utf-8?q?To_Example?= <"to@other.com"@example.com>', + 'To Example <"to@other.com"@example.com>', + ), + # Addresses with long unicode display names. + ( + 'Tó Example very long' * 4 + ' <to@example.com>', + 'utf-8', + '=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT' + '=C3=B3_Example_?=\n' + ' =?utf-8?q?very_longT=C3=B3_Example_very_long?= ' + '<to@example.com>', + ), + ( + ('Tó Example very long' * 4, 'to@example.com'), + 'utf-8', + '=?utf-8?q?T=C3=B3_Example_very_longT=C3=B3_Example_very_longT' + '=C3=B3_Example_?=\n' + ' =?utf-8?q?very_longT=C3=B3_Example_very_long?= ' + '<to@example.com>', ), + # Address with long display name and unicode domain. + ( + ('To Example very long' * 4, 'to@exampl€.com'), + 'utf-8', + 'To Example very longTo Example very longTo Example very longT' + 'o Example very\n' + ' long <to@xn--exampl-nc1c.com>' + ) ): with self.subTest(email_address=email_address, encoding=encoding): self.assertEqual(sanitize_address(email_address, encoding), expected_result) @@ -781,6 +806,19 @@ class MailTests(HeadersCheckMixin, SimpleTestCase): with self.assertRaises(ValueError): sanitize_address(email_address, encoding='utf-8') + def test_sanitize_address_header_injection(self): + msg = 'Invalid address; address parts cannot contain newlines.' + tests = [ + 'Name\nInjection <to@example.com>', + ('Name\nInjection', 'to@xample.com'), + 'Name <to\ninjection@example.com>', + ('Name', 'to\ninjection@example.com'), + ] + for email_address in tests: + with self.subTest(email_address=email_address): + with self.assertRaisesMessage(ValueError, msg): + sanitize_address(email_address, encoding='utf-8') + @requires_tz_support class MailTimeZoneTests(SimpleTestCase): |