From 031ea9a660bc694310b8a187c964694fe9b4a100 Mon Sep 17 00:00:00 2001 From: prodigysml Date: Thu, 7 Sep 2023 22:42:00 +1000 Subject: [PATCH 1/5] Fixed a regular expression denial of service issue by limiting whitespaces --- pydantic/networks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydantic/networks.py b/pydantic/networks.py index a1486b2265..debbe056b0 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -420,7 +420,8 @@ def _build_pretty_email_regex() -> re.Pattern: name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]' unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)' quoted_name_group = r'"((?:[^"]|\")+)"' - email_group = r'<\s*(.+)\s*>' + # altered regex to no longer be susceptible to denial of service + email_group = r'<\s{0,5}.\s{0,5}>' return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') From b27b96cca12bcc3d6f5ef95e55c8469965c87bc7 Mon Sep 17 00:00:00 2001 From: prodigysml Date: Thu, 7 Sep 2023 22:57:48 +1000 Subject: [PATCH 2/5] improved a bit, but not perfect --- pydantic/networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/networks.py b/pydantic/networks.py index debbe056b0..69405eb2ab 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -421,7 +421,7 @@ def _build_pretty_email_regex() -> re.Pattern: unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)' quoted_name_group = r'"((?:[^"]|\")+)"' # altered regex to no longer be susceptible to denial of service - email_group = r'<\s{0,5}.\s{0,5}>' + email_group = r'<\s{0,5}(.+)\s{0,5}>' return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') From 29f2a691028edd2888cf2d9e6f391e7b24cf9e46 Mon Sep 17 00:00:00 2001 From: prodigysml Date: Thu, 7 Sep 2023 23:05:09 +1000 Subject: [PATCH 3/5] further improved. Should be fully patched --- pydantic/networks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/networks.py b/pydantic/networks.py index 69405eb2ab..7c3098cdac 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -421,7 +421,7 @@ def _build_pretty_email_regex() -> re.Pattern: unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)' quoted_name_group = r'"((?:[^"]|\")+)"' # altered regex to no longer be susceptible to denial of service - email_group = r'<\s{0,5}(.+)\s{0,5}>' + email_group = r'<\s{0,5}(.{0,3000})\s{0,5}>' return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') From 64cd8266602f5ffa898f938681be3626df345384 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 20 Sep 2023 05:08:16 -0400 Subject: [PATCH 4/5] Guard against length itself --- pydantic/networks.py | 17 ++++++++++++++--- tests/test_networks.py | 7 ++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pydantic/networks.py b/pydantic/networks.py index 7c3098cdac..05f439eced 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -416,17 +416,21 @@ def _validate(cls, __input_value: NetworkType, _: core_schema.ValidationInfo) -> return cls(__input_value) # type: ignore[return-value] -def _build_pretty_email_regex() -> re.Pattern: +def _build_pretty_email_regex() -> re.Pattern[str]: name_chars = r'[\w!#$%&\'*+\-/=?^_`{|}~]' unquoted_name_group = fr'((?:{name_chars}+\s+)*{name_chars}+)' quoted_name_group = r'"((?:[^"]|\")+)"' - # altered regex to no longer be susceptible to denial of service - email_group = r'<\s{0,5}(.{0,3000})\s{0,5}>' + email_group = r'<\s*(.{0,254})\s*>' return re.compile(rf'\s*(?:{unquoted_name_group}|{quoted_name_group})?\s*{email_group}\s*') pretty_email_regex = _build_pretty_email_regex() +MAX_EMAIL_LENGTH = 2048 +"""Maximum length for an email. +A somewhat arbitrary but very generous number compared to what is allowed by most implementations. +""" + def validate_email(value: str) -> tuple[str, str]: """Email address validation using [email-validator](https://pypi.org/project/email-validator/). @@ -441,6 +445,13 @@ def validate_email(value: str) -> tuple[str, str]: if email_validator is None: import_email_validator() + if len(value) > MAX_EMAIL_LENGTH: + raise PydanticCustomError( + 'value_error', + 'value is not a valid email address: {reason}', + {'reason': f'Length must not exceed {MAX_EMAIL_LENGTH} characters'}, + ) + m = pretty_email_regex.fullmatch(value) name: str | None = None if m: diff --git a/tests/test_networks.py b/tests/test_networks.py index 777ec7e120..3338b3f04e 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -1,4 +1,4 @@ -from typing import Union +from __future__ import annotations import pytest from pydantic_core import PydanticCustomError, Url @@ -277,7 +277,7 @@ class Model(BaseModel): def test_nullable_http_url(): class Model(BaseModel): - v: Union[HttpUrl, None] + v: HttpUrl | None assert Model(v=None).v is None assert str(Model(v='http://example.org').v) == 'http://example.org/' @@ -812,9 +812,10 @@ def test_address_valid(value, name, email): ('foobar <', None), ('foobar <>', None), ('first.last ', None), + pytest.param('foobar <' + 'a' * 4096 + '@example.com>', 'Length must not exceed 2048 characters', id='long'), ], ) -def test_address_invalid(value, reason): +def test_address_invalid(value: str, reason: str | None): with pytest.raises(PydanticCustomError, match=f'value is not a valid email address: {reason or ""}'): validate_email(value) From bef97769394dd96feafcdd9fd2830cfa4bb58bf0 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 20 Sep 2023 05:12:51 -0400 Subject: [PATCH 5/5] Fix type hints --- tests/test_networks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_networks.py b/tests/test_networks.py index 3338b3f04e..852c7763d3 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from typing import Union import pytest from pydantic_core import PydanticCustomError, Url @@ -277,7 +277,7 @@ class Model(BaseModel): def test_nullable_http_url(): class Model(BaseModel): - v: HttpUrl | None + v: Union[HttpUrl, None] assert Model(v=None).v is None assert str(Model(v='http://example.org').v) == 'http://example.org/' @@ -815,7 +815,7 @@ def test_address_valid(value, name, email): pytest.param('foobar <' + 'a' * 4096 + '@example.com>', 'Length must not exceed 2048 characters', id='long'), ], ) -def test_address_invalid(value: str, reason: str | None): +def test_address_invalid(value: str, reason: Union[str, None]): with pytest.raises(PydanticCustomError, match=f'value is not a valid email address: {reason or ""}'): validate_email(value)