Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2120089

Browse filesBrowse files
[3.13] gh-80222: Fix email address header folding with long quoted-string (GH-122753) (#129007)
gh-80222: Fix email address header folding with long quoted-string (GH-122753) 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. (cherry picked from commit 5aaf416) Co-authored-by: Mike Edmunds <medmunds@gmail.com>
1 parent 6b47499 commit 2120089
Copy full SHA for 2120089

File tree

Expand file treeCollapse file tree

3 files changed

+53
-3
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+53
-3
lines changed

‎Lib/email/_header_value_parser.py

Copy file name to clipboardExpand all lines: Lib/email/_header_value_parser.py
+18-1Lines changed: 18 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 make_quoted_pairs(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 = make_quoted_pairs(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,15 @@ 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+
# To fold a quoted string we need to create a list of terminal
2918+
# tokens that will render the leading and trailing quotes
2919+
# and use quoted pairs in the value as appropriate.
2920+
newparts = (
2921+
[ValueTerminal('"', 'ptext')] +
2922+
[ValueTerminal(make_quoted_pairs(p), 'ptext')
2923+
for p in newparts] +
2924+
[ValueTerminal('"', 'ptext')])
29082925
if not part.as_ew_allowed:
29092926
wrap_as_ew_blocked += 1
29102927
newparts.append(end_ew_not_allowed)

‎Lib/test/test_email/test__header_value_parser.py

Copy file name to clipboardExpand all lines: Lib/test/test_email/test__header_value_parser.py
+29-2Lines 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 <beautiful@example.com>\n')
30843084

3085-
a = '.' * 79
3085+
a = '.' * 79 # ('.' is a special, so must be in quoted-string.)
30863086
to = f'"{a}" <xyz@example.com>, "Hübsch Kaktus" <beautiful@example.com>'
30873087
self._test(parser.get_address_list(to)[0],
3088-
f'{a}\n'
3088+
f'"{a}"\n'
30893089
' <xyz@example.com>, =?utf-8?q?H=C3=BCbsch?= Kaktus '
30903090
'<beautiful@example.com>\n')
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 <spy@example.org> (unclosed comment?" <to@example.com>',
3098+
'"Exfiltrator <spy@example.org> (unclosed\n'
3099+
' comment?" <to@example.com>\n'),
3100+
('"Escaped \\" chars \\\\ in quoted-string stay escaped" <to@example.com>',
3101+
'"Escaped \\" chars \\\\ in quoted-string\n'
3102+
' stay escaped" <to@example.com>\n'),
3103+
('This long display name does not need quotes <to@example.com>',
3104+
'This long display name does not need\n'
3105+
' quotes <to@example.com>\n'),
3106+
('"Quotes are not required but are retained here" <to@example.com>',
3107+
'"Quotes are not required but are\n'
3108+
' retained here" <to@example.com>\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+
('"local-part-with-specials@but-no-fws.cannot-fold"@example.com',
3113+
'"local-part-with-specials@but-no-fws.cannot-fold"@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.
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix bug in the folding of quoted strings when flattening an email message using
2+
a modern email policy. Previously when a quoted string was folded so that
3+
it spanned more than one line, the surrounding quotes and internal escapes
4+
would be omitted. This could theoretically be used to spoof header lines
5+
using a carefully constructed quoted string if the resulting rendered email
6+
was transmitted or re-parsed.

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.