diff --git a/api/libs/helper.py b/api/libs/helper.py index 7066f9eab45..752342bfad6 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -221,8 +221,11 @@ def current_timestamp() -> int: def email(email): # Define a regex pattern for email addresses pattern = r"^[\w\.!#$%&'*+\-/=?^_`{|}~]+@([\w-]+\.)+[\w-]{2,}$" - # Check if the email matches the pattern - if re.match(pattern, email) is not None: + # Use re.fullmatch instead of re.match to reject trailing newlines. + # In Python, '$' matches at end-of-string OR just before a trailing newline, + # so re.match accepts "user@example.com\n". re.fullmatch requires the entire + # string to match, closing the mail header-injection vector. (#39234) + if re.fullmatch(pattern, email) is not None: return email error = f"{email} is not a valid email." diff --git a/api/tests/unit_tests/libs/test_helper.py b/api/tests/unit_tests/libs/test_helper.py index 1a93dbbca13..dbc2cd6cba8 100644 --- a/api/tests/unit_tests/libs/test_helper.py +++ b/api/tests/unit_tests/libs/test_helper.py @@ -2,7 +2,7 @@ from datetime import datetime import pytest -from libs.helper import OptionalTimestampField, escape_like_pattern, extract_tenant_id +from libs.helper import OptionalTimestampField, email, escape_like_pattern, extract_tenant_id from models.account import Account from models.model import EndUser @@ -126,3 +126,30 @@ class TestEscapeLikePattern: result = escape_like_pattern("test\\%_value") # Should be: test\\\%\_value assert result == "test\\\\\\%\\_value" + + +class TestEmailValidator: + """Tests for the email() validator — regression for #39234.""" + + def test_valid_email_accepted(self): + assert email("user@example.com") == "user@example.com" + + def test_trailing_newline_rejected(self): + with pytest.raises(ValueError, match="not a valid email"): + email("user@example.com\n") + + def test_trailing_carriage_return_newline_rejected(self): + with pytest.raises(ValueError, match="not a valid email"): + email("user@example.com\r\n") + + def test_multiple_newlines_rejected(self): + with pytest.raises(ValueError, match="not a valid email"): + email("user@example.com\n\n") + + def test_empty_string_rejected(self): + with pytest.raises(ValueError, match="not a valid email"): + email("") + + def test_invalid_email_rejected(self): + with pytest.raises(ValueError, match="not a valid email"): + email("not-an-email")