diff --git a/Lib/email/header.py b/Lib/email/header.py index ea1f84ea964ca60..ab61d64c292e044 100644 --- a/Lib/email/header.py +++ b/Lib/email/header.py @@ -49,7 +49,10 @@ # Find a header embedded in a putative header value. Used to check for # header injection attack. -_embedded_header = re.compile(r'\n[^ \t]+:') +# A header name may not contain whitespace, but receivers are lenient about +# whitespace before the colon, so an injected line using it still reaches +# them as a header. Match it here too (gh-76787). +_embedded_header = re.compile(r'\n[^ \t]+[ \t]*:') # Helpers diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index e40c82bba9af426..0b003d2f2ca968c 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -851,6 +851,27 @@ def test_embedded_header_via_string_rejected(self): msg['Dummy'] = 'dummy\nX-Injected-Header: test' self.assertRaises(errors.HeaderParseError, msg.as_string) + # gh-76787: receivers accept whitespace between a header name and the + # colon, so a line using it is an injected header too. + def test_embedded_header_with_space_before_colon_rejected(self): + for injected in ('dummy\nX-Injected-Header : test', + 'dummy\nX-Injected-Header\t: test', + 'dummy\nX-Injected-Header \t : test'): + with self.subTest(injected=injected): + msg = Message() + msg['Dummy'] = Header(injected) + self.assertRaises(errors.HeaderParseError, msg.as_string) + + msg = Message() + msg['Dummy'] = injected + self.assertRaises(errors.HeaderParseError, msg.as_string) + + def test_folded_continuation_line_still_accepted(self): + # A continuation line starts with whitespace and is not an injection. + msg = Message() + msg['Dummy'] = Header('dummy\n continued here: not a header') + self.assertIn('continued here', msg.as_string()) + def test_unicode_header_defaults_to_utf8_encoding(self): # Issue 14291 m = MIMEText('abc\n') diff --git a/Misc/NEWS.d/next/Library/2026-09-25-12-40-00.gh-issue-76787.Kx9mQa.rst b/Misc/NEWS.d/next/Library/2026-09-25-12-40-00.gh-issue-76787.Kx9mQa.rst new file mode 100644 index 000000000000000..45580d940ef7180 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-25-12-40-00.gh-issue-76787.Kx9mQa.rst @@ -0,0 +1,5 @@ +:class:`email.header.Header` now rejects an embedded header whose name is +separated from the colon by spaces or tabs. Receivers accept that form, so a +value such as ``"addr@example.com\ncc : injected@example.com"`` reached them +as two headers while the check that was meant to catch it only looked for a +colon immediately after the name.