Skip to content

Commit c432d01

Browse files
srinivasreddypicnixzbitdancer
authored
gh-127794: Validate email header names according to RFC 5322 (#127820)
`email.message.Message` objects now validate header names specified via `__setitem__` or `add_header` according to RFC 5322, §2.2 [1]. In particular, callers should expect a ValueError to be raised for invalid header names. [1]: https://datatracker.ietf.org/doc/html/rfc5322#section-2.2 --------- Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: R. David Murray <[email protected]>
1 parent 55150a7 commit c432d01

File tree

5 files changed

+71
-1
lines changed

5 files changed

+71
-1
lines changed

Lib/email/_policybase.py

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55

66
import abc
7+
import re
78
from email import header
89
from email import charset as _charset
910
from email.utils import _has_surrogates
@@ -14,6 +15,14 @@
1415
'compat32',
1516
]
1617

18+
# validation regex from RFC 5322, equivalent to pattern re.compile("[!-9;-~]+$")
19+
valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
20+
21+
def validate_header_name(name):
22+
# Validate header name according to RFC 5322
23+
if not valid_header_name_re.match(name):
24+
raise ValueError(
25+
f"Header field name contains invalid characters: {name!r}")
1726

1827
class _PolicyBase:
1928

@@ -314,6 +323,7 @@ def header_store_parse(self, name, value):
314323
"""+
315324
The name and value are returned unmodified.
316325
"""
326+
validate_header_name(name)
317327
return (name, value)
318328

319329
def header_fetch_parse(self, name, value):

Lib/email/policy.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
import re
66
import sys
7-
from email._policybase import Policy, Compat32, compat32, _extend_docstrings
7+
from email._policybase import (
8+
Compat32,
9+
Policy,
10+
_extend_docstrings,
11+
compat32,
12+
validate_header_name
13+
)
814
from email.utils import _has_surrogates
915
from email.headerregistry import HeaderRegistry as HeaderRegistry
1016
from email.contentmanager import raw_data_manager
@@ -138,6 +144,7 @@ def header_store_parse(self, name, value):
138144
CR or LF characters.
139145
140146
"""
147+
validate_header_name(name)
141148
if hasattr(value, 'name') and value.name.lower() == name.lower():
142149
return (name, value)
143150
if isinstance(value, str) and len(value.splitlines())>1:

Lib/test/test_email/test_email.py

+25
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,31 @@ def test_nonascii_add_header_with_tspecial(self):
728728
"attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
729729
msg['Content-Disposition'])
730730

731+
def test_invalid_header_names(self):
732+
invalid_headers = [
733+
('Invalid Header', 'contains space'),
734+
('Tab\tHeader', 'contains tab'),
735+
('Colon:Header', 'contains colon'),
736+
('', 'Empty name'),
737+
(' LeadingSpace', 'starts with space'),
738+
('TrailingSpace ', 'ends with space'),
739+
('Header\x7F', 'Non-ASCII character'),
740+
('Header\x80', 'Extended ASCII'),
741+
]
742+
for policy in (email.policy.default, email.policy.compat32):
743+
for setter in (Message.__setitem__, Message.add_header):
744+
for name, value in invalid_headers:
745+
self.do_test_invalid_header_names(
746+
policy, setter,name, value)
747+
748+
def do_test_invalid_header_names(self, policy, setter, name, value):
749+
with self.subTest(policy=policy, setter=setter, name=name, value=value):
750+
message = Message(policy=policy)
751+
pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
752+
with self.assertRaisesRegex(ValueError, pattern) as cm:
753+
setter(message, name, value)
754+
self.assertIn(f"{name!r}", str(cm.exception))
755+
731756
def test_binary_quopri_payload(self):
732757
for charset in ('latin-1', 'ascii'):
733758
msg = Message()

Lib/test/test_email/test_message.py

+24
Original file line numberDiff line numberDiff line change
@@ -1004,6 +1004,30 @@ def test_folding_with_long_nospace_http_policy_1(self):
10041004
parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default)
10051005
self.assertEqual(parsed_msg['Message-ID'], m['Message-ID'])
10061006

1007+
def test_invalid_header_names(self):
1008+
invalid_headers = [
1009+
('Invalid Header', 'contains space'),
1010+
('Tab\tHeader', 'contains tab'),
1011+
('Colon:Header', 'contains colon'),
1012+
('', 'Empty name'),
1013+
(' LeadingSpace', 'starts with space'),
1014+
('TrailingSpace ', 'ends with space'),
1015+
('Header\x7F', 'Non-ASCII character'),
1016+
('Header\x80', 'Extended ASCII'),
1017+
]
1018+
for email_policy in (policy.default, policy.compat32):
1019+
for setter in (EmailMessage.__setitem__, EmailMessage.add_header):
1020+
for name, value in invalid_headers:
1021+
self.do_test_invalid_header_names(email_policy, setter, name, value)
1022+
1023+
def do_test_invalid_header_names(self, policy, setter, name, value):
1024+
with self.subTest(policy=policy, setter=setter, name=name, value=value):
1025+
message = EmailMessage(policy=policy)
1026+
pattern = r'(?i)(?=.*invalid)(?=.*header)(?=.*name)'
1027+
with self.assertRaisesRegex(ValueError, pattern) as cm:
1028+
setter(message, name, value)
1029+
self.assertIn(f"{name!r}", str(cm.exception))
1030+
10071031
def test_get_body_malformed(self):
10081032
"""test for bpo-42892"""
10091033
msg = textwrap.dedent("""\
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When headers are added to :class:`email.message.Message` objects, either through
2+
:meth:`email.message.Message.__setitem__` or :meth:`email.message.Message.add_header`,
3+
the field name is now validated according to :rfc:`RFC 5322, Section 2.2 <5322#section-2.2>`
4+
and a :exc:`ValueError` is raised if the field name contains any invalid characters.

0 commit comments

Comments
 (0)