From 0f8984c2ba6eb9c9c65a3a4eef85fe7e342a72d3 Mon Sep 17 00:00:00 2001 From: Keerthana KT Date: Fri, 6 Nov 2026 18:01:50 +0530 Subject: [PATCH] fix quadratic time in split_email on long input split_string_at_unquoted_special normalized text[i:] at every position to see whether a character combines with the ones after it, which copies and normalizes the rest of the string each time. limit the lookahead to a 32-character window so parsing is linear in the length of the input. --- email_validator/syntax.py | 9 ++++++++- tests/test_syntax.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/email_validator/syntax.py b/email_validator/syntax.py index 6af2095..4e7a0be 100644 --- a/email_validator/syntax.py +++ b/email_validator/syntax.py @@ -61,7 +61,14 @@ def split_string_at_unquoted_special(text: str, specials: tuple[str, ...]) -> tu # the < as the start of "" syntax in that case. Likewise, # if anything combines with an @ or ", we should probably not # treat it as a special character. - if unicodedata.normalize("NFC", text[i:])[0] != c: + # + # Only the characters right after c can combine with it, so + # look at a short window rather than normalizing the rest of + # the string at every position, which is quadratic in the + # length of the input. The window is c, the 30 combining + # characters in a row allowed by the Unicode Stream-Safe Text + # Format (UAX #15), and one more character. + if unicodedata.normalize("NFC", text[i:i + 32])[0] != c: left_part += c elif inside_quote: diff --git a/tests/test_syntax.py b/tests/test_syntax.py index efaf9d3..4580e7f 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -1,3 +1,4 @@ +import time from typing import Any import pytest @@ -519,6 +520,7 @@ def test_domain_literal() -> None: (' !', 'There can\'t be anything after the email address.'), ('<\u0338me@example.com', 'The email address contains invalid characters before the @-sign: \'<\'.'), + ('<\u0301\u0338me@example.com', 'The email address contains invalid characters before the @-sign: \'<\'.'), ('DisplayName ', 'An email address cannot have a hyphen immediately after the @-sign.'), ('DisplayName ', 'A display name and angle brackets around the email address are not permitted here.'), ('Display Name ', 'A display name and angle brackets around the email address are not permitted here.'), @@ -535,6 +537,15 @@ def test_email_invalid_syntax(email_input: str, error_msg: str) -> None: assert str(exc_info.value) == error_msg +def test_email_long_input_is_not_quadratic() -> None: + # split_email used to NFC-normalize the rest of the string at every + # character, so rejecting this took about 15 seconds. + start = time.perf_counter() + with pytest.raises(EmailSyntaxError, match="unsafe characters"): + validate_email("\u0301" * 50000 + "@example.com", check_deliverability=False) + assert time.perf_counter() - start < 2 + + @pytest.mark.parametrize( 'email_input,error_msg', [