Skip to content

Commit 73f3ffd

Browse files
committed
gh-80222: Fix email address header folding with long quoted-string
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.
1 parent c4e8196 commit 73f3ffd

File tree

3 files changed

+49
-3
lines changed

3 files changed

+49
-3
lines changed

Lib/email/_header_value_parser.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,16 @@
9595
NLSET = {'\n', '\r'}
9696
SPECIALSNL = SPECIALS | NLSET
9797

98+
99+
def escape_for_quotes(value):
100+
"""Escape dquote and backslash for use within a quoted-string."""
101+
return str(value).replace('\\', '\\\\').replace('"', '\\"')
102+
103+
98104
def quote_string(value):
99-
return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
105+
escaped = escape_for_quotes(value)
106+
return f'"{escaped}"'
107+
100108

101109
# Match a RFC 2047 word, looks like =?utf-8?q?someword?=
102110
rfc2047_matcher = re.compile(r'''
@@ -2905,6 +2913,14 @@ def _refold_parse_tree(parse_tree, *, policy):
29052913
if not hasattr(part, 'encode'):
29062914
# It's not a terminal, try folding the subparts.
29072915
newparts = list(part)
2916+
if part.token_type == 'bare-quoted-string':
2917+
# Restore the quotes and escape contents.
2918+
dquote = ValueTerminal('"', 'ptext')
2919+
newparts = (
2920+
[dquote] +
2921+
[ValueTerminal(escape_for_quotes(p), 'ptext')
2922+
for p in newparts] +
2923+
[dquote])
29082924
if not part.as_ew_allowed:
29092925
wrap_as_ew_blocked += 1
29102926
newparts.append(end_ew_not_allowed)

Lib/test/test_email/test__header_value_parser.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3082,13 +3082,40 @@ def test_address_list_with_list_separator_after_fold(self):
30823082
self._test(parser.get_address_list(to)[0],
30833083
f'{a},\n =?utf-8?q?H=C3=BCbsch?= Kaktus <[email protected]>\n')
30843084

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

3092+
def test_address_list_with_specials_in_long_quoted_string(self):
3093+
# Regression for gh-80222.
3094+
policy = self.policy.clone(max_line_length=40)
3095+
cases = [
3096+
# (to, folded)
3097+
('"Exfiltrator <[email protected]> (unclosed comment?" <[email protected]>',
3098+
'"Exfiltrator <[email protected]> (unclosed\n'
3099+
' comment?" <[email protected]>\n'),
3100+
('"Escaped \\" chars \\\\ in quoted-string stay escaped" <[email protected]>',
3101+
'"Escaped \\" chars \\\\ in quoted-string\n'
3102+
' stay escaped" <[email protected]>\n'),
3103+
('This long display name does not need quotes <[email protected]>',
3104+
'This long display name does not need\n'
3105+
' quotes <[email protected]>\n'),
3106+
('"Quotes are not required but are retained here" <[email protected]>',
3107+
'"Quotes are not required but are\n'
3108+
' retained here" <[email protected]>\n'),
3109+
('"A quoted-string, it can be a valid local-part"@example.com',
3110+
'"A quoted-string, it can be a valid\n'
3111+
' local-part"@example.com\n'),
3112+
('"[email protected]"@example.com',
3113+
'"[email protected]"@example.com\n'),
3114+
]
3115+
for (to, folded) in cases:
3116+
with self.subTest(to=to):
3117+
self._test(parser.get_address_list(to)[0], folded, policy=policy)
3118+
30923119
# XXX Need tests with comments on various sides of a unicode token,
30933120
# and with unicode tokens in the comments. Spaces inside the quotes
30943121
# currently don't do the right thing.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a problem where email.policy.default header refolding could incorrectly
2+
omit quotes from structured email headers, enabling sender or recipient
3+
spoofing via a carefully crafted display-name.

0 commit comments

Comments
 (0)