Skip to content

Commit

Permalink
pythongh-80222: Fix email address header folding with long quoted-string
Browse files Browse the repository at this point in the history
Email generators using email.policy.default could incorrectly omit the
quote ('"') characters from a quoted-string during header refolding,
leading to invalid address headers and enabling header spoofing. This
change restores the quote characters on a bare-quoted-string as the
header is refolded, and escapes backslash and quote chars in the string.
  • Loading branch information
medmunds committed Aug 6, 2024
1 parent c4e8196 commit 73f3ffd
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 3 deletions.
18 changes: 17 additions & 1 deletion Lib/email/_header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,16 @@
NLSET = {'\n', '\r'}
SPECIALSNL = SPECIALS | NLSET


def escape_for_quotes(value):
"""Escape dquote and backslash for use within a quoted-string."""
return str(value).replace('\\', '\\\\').replace('"', '\\"')


def quote_string(value):
return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
escaped = escape_for_quotes(value)
return f'"{escaped}"'


# Match a RFC 2047 word, looks like =?utf-8?q?someword?=
rfc2047_matcher = re.compile(r'''
Expand Down Expand Up @@ -2905,6 +2913,14 @@ def _refold_parse_tree(parse_tree, *, policy):
if not hasattr(part, 'encode'):
# It's not a terminal, try folding the subparts.
newparts = list(part)
if part.token_type == 'bare-quoted-string':
# Restore the quotes and escape contents.
dquote = ValueTerminal('"', 'ptext')
newparts = (
[dquote] +
[ValueTerminal(escape_for_quotes(p), 'ptext')
for p in newparts] +
[dquote])
if not part.as_ew_allowed:
wrap_as_ew_blocked += 1
newparts.append(end_ew_not_allowed)
Expand Down
31 changes: 29 additions & 2 deletions Lib/test/test_email/test__header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3082,13 +3082,40 @@ def test_address_list_with_list_separator_after_fold(self):
self._test(parser.get_address_list(to)[0],
f'{a},\n =?utf-8?q?H=C3=BCbsch?= Kaktus <[email protected]>\n')

a = '.' * 79
a = '.' * 79 # ('.' is a special, so must be in quoted-string.)
to = f'"{a}" <[email protected]>, "Hübsch Kaktus" <[email protected]>'
self._test(parser.get_address_list(to)[0],
f'{a}\n'
f'"{a}"\n'
' <[email protected]>, =?utf-8?q?H=C3=BCbsch?= Kaktus '
'<[email protected]>\n')

def test_address_list_with_specials_in_long_quoted_string(self):
# Regression for gh-80222.
policy = self.policy.clone(max_line_length=40)
cases = [
# (to, folded)
('"Exfiltrator <[email protected]> (unclosed comment?" <[email protected]>',
'"Exfiltrator <[email protected]> (unclosed\n'
' comment?" <[email protected]>\n'),
('"Escaped \\" chars \\\\ in quoted-string stay escaped" <[email protected]>',
'"Escaped \\" chars \\\\ in quoted-string\n'
' stay escaped" <[email protected]>\n'),
('This long display name does not need quotes <[email protected]>',
'This long display name does not need\n'
' quotes <[email protected]>\n'),
('"Quotes are not required but are retained here" <[email protected]>',
'"Quotes are not required but are\n'
' retained here" <[email protected]>\n'),
('"A quoted-string, it can be a valid local-part"@example.com',
'"A quoted-string, it can be a valid\n'
' local-part"@example.com\n'),
('"[email protected]"@example.com',
'"[email protected]"@example.com\n'),
]
for (to, folded) in cases:
with self.subTest(to=to):
self._test(parser.get_address_list(to)[0], folded, policy=policy)

# XXX Need tests with comments on various sides of a unicode token,
# and with unicode tokens in the comments. Spaces inside the quotes
# currently don't do the right thing.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a problem where email.policy.default header refolding could incorrectly
omit quotes from structured email headers, enabling sender or recipient
spoofing via a carefully crafted display-name.

0 comments on commit 73f3ffd

Please sign in to comment.