From 168b5f0cd9cee5a7639aced6ea727f76d5422b6d Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 27 Oct 2025 12:06:32 +0100 Subject: [PATCH 1/8] Disallow non-ASCII in email domain --- server/mergin/auth/forms.py | 20 +++++++++++++++++--- server/mergin/tests/test_auth.py | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index b45638eb..99a4232e 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -48,9 +48,12 @@ class ExtendedEmail(Email): 1. spaces, 2. special characters ,:;()<>[]\" 3, multiple @ symbols, - 4, leading, trailing, or consecutive dots in the local part - 5, invalid domain part - missing top level domain (user@example), consecutive dots - Custom check for additional invalid characters disallows |'— because they make our email sending service to fail + 4, leading, trailing, or consecutive dots in the local part, + 5, invalid domain part - missing top level domain (user@example), consecutive dots, + Custom check for + - additional invalid characters disallows |'— + - non-ASCII characters in the domain part + because they make our email sending service to fail """ def __call__(self, form, field): @@ -61,6 +64,17 @@ def __call__(self, form, field): f"Email address '{field.data}' contains an invalid character." ) + try: + local_part, domain_part = field.data.rsplit("@", 1) + except ValueError: + raise ValidationError(f"Invalid email address '{field.data}'.") + + # character is one of the standard ASCII characters (0–127) + if not all(ord(c) < 128 for c in domain_part): + raise ValidationError( + f"Email address '{field.data}' contains non-ASCII characters in the domain part." + ) + class PasswordValidator: def __init__(self, min_length=8): diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index d53b01bc..b953577d 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -128,6 +128,7 @@ def test_logout(client): ("verylonglonglonglonglonglonglongemail@example.com", "#pwd1234", 201), ("us.er@mergin.com", "#pwd1234", 201), # dot is allowed ("us er@mergin.com", "#pwd1234", 400), # space is disallowed + ("test@gmaiñ.com", "#pwd1234", 400), # non-ASCII character in the domain ] From 8635e5ee29b9517b1c0dd44e7ee283556bac1690 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Mon, 3 Nov 2025 12:08:44 +0100 Subject: [PATCH 2/8] use email_validator and isascii --- server/mergin/auth/forms.py | 10 +++------- server/mergin/auth/utils.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 server/mergin/auth/utils.py diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index 99a4232e..884d0dad 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -17,6 +17,7 @@ from .models import MAX_USERNAME_LENGTH, User from ..app import UpdateForm, CustomStringField +from .utils import get_email_domain def username_validation(form, field): @@ -64,13 +65,8 @@ def __call__(self, form, field): f"Email address '{field.data}' contains an invalid character." ) - try: - local_part, domain_part = field.data.rsplit("@", 1) - except ValueError: - raise ValidationError(f"Invalid email address '{field.data}'.") - - # character is one of the standard ASCII characters (0–127) - if not all(ord(c) < 128 for c in domain_part): + domain = get_email_domain(field.data) + if not domain.isascii(): raise ValidationError( f"Email address '{field.data}' contains non-ASCII characters in the domain part." ) diff --git a/server/mergin/auth/utils.py b/server/mergin/auth/utils.py new file mode 100644 index 00000000..b81b2eca --- /dev/null +++ b/server/mergin/auth/utils.py @@ -0,0 +1,13 @@ +# Copyright (C) Lutra Consulting Limited +# +# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial + +from email_validator import validate_email, EmailNotValidError + + +def get_email_domain(email: str) -> str | None: + try: + result = validate_email(email, check_deliverability=False) + return result.domain + except EmailNotValidError: + return From 178020c28f3ad49b84c47a3827e4d57ad4cffb2a Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Tue, 4 Nov 2025 15:26:51 +0100 Subject: [PATCH 3/8] use supplied regex --- server/Pipfile | 1 + server/Pipfile.lock | 124 +++++++++++++++++++++++++++++++++++- server/mergin/auth/forms.py | 25 ++++---- 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/server/Pipfile b/server/Pipfile index 9859c84e..397f4b69 100644 --- a/server/Pipfile +++ b/server/Pipfile @@ -42,6 +42,7 @@ typing_extensions = "==4.12.2" python-magic = "==0.4.27" # requirements for development on windows colorama = "==0.4.5" +regex = "2025.11.3" [dev-packages] pytest = "==8.3.2" diff --git a/server/Pipfile.lock b/server/Pipfile.lock index 1e02fadf..27026077 100644 --- a/server/Pipfile.lock +++ b/server/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5d7a107dfa425254eebbe86700e139290dc6986e54bd18e1538a2d87a60282f3" + "sha256": "e097e0123cf67b8856804f44a67ba31e6c7e2c28ebf81c0884883f59d9a2a130" }, "pipfile-spec": 6, "requires": { @@ -994,6 +994,128 @@ "markers": "python_version >= '3.9'", "version": "==0.36.2" }, + "regex": { + "hashes": [ + "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", + "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", + "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", + "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", + "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", + "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", + "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", + "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", + "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", + "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", + "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", + "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", + "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", + "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", + "sha256:22dd622a402aad4558277305350699b2be14bc59f64d64ae1d928ce7d072dced", + "sha256:22e7d1cdfa88ef33a2ae6aa0d707f9255eb286ffbd90045f1088246833223aee", + "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", + "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", + "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", + "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", + "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", + "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", + "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", + "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", + "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", + "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", + "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", + "sha256:3bf28b1873a8af8bbb58c26cc56ea6e534d80053b41fb511a35795b6de507e6a", + "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", + "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", + "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", + "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", + "sha256:43b4fb020e779ca81c1b5255015fe2b82816c76ec982354534ad9ec09ad7c9e3", + "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", + "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", + "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", + "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", + "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", + "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", + "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", + "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", + "sha256:51c1c1847128238f54930edb8805b660305dca164645a9fd29243f5610beea34", + "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", + "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", + "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", + "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", + "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", + "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", + "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", + "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", + "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", + "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", + "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", + "sha256:732aea6de26051af97b94bc98ed86448821f839d058e5d259c72bf6d73ad0fc0", + "sha256:74d04244852ff73b32eeede4f76f51c5bcf44bc3c207bc3e6cf1c5c45b890708", + "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", + "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", + "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", + "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", + "sha256:7a50cd39f73faa34ec18d6720ee25ef10c4c1839514186fcda658a06c06057a2", + "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", + "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", + "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", + "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", + "sha256:81519e25707fc076978c6143b81ea3dc853f176895af05bf7ec51effe818aeec", + "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", + "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", + "sha256:856a25c73b697f2ce2a24e7968285579e62577a048526161a2c0f53090bea9f9", + "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", + "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", + "sha256:8a3d571bd95fade53c86c0517f859477ff3a93c3fde10c9e669086f038e0f207", + "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", + "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", + "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", + "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", + "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", + "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", + "sha256:a295ca2bba5c1c885826ce3125fa0b9f702a1be547d821c01d65f199e10c01e2", + "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", + "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", + "sha256:b4774ff32f18e0504bfc4e59a3e71e18d83bc1e171a3c8ed75013958a03b2f14", + "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", + "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", + "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", + "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", + "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", + "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", + "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", + "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", + "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", + "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", + "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", + "sha256:cc4076a5b4f36d849fd709284b4a3b112326652f3b0466f04002a6c15a0c96c1", + "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", + "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", + "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", + "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", + "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", + "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", + "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", + "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", + "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", + "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", + "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", + "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", + "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", + "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", + "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", + "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", + "sha256:f3b5a391c7597ffa96b41bd5cbd2ed0305f515fcbb367dfa72735679d5502364", + "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", + "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", + "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", + "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", + "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2025.11.3" + }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index 884d0dad..e5339b77 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -1,7 +1,8 @@ # Copyright (C) Lutra Consulting Limited # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import re + +import regex import safe from flask_wtf import FlaskForm from sqlalchemy import func @@ -17,7 +18,6 @@ from .models import MAX_USERNAME_LENGTH, User from ..app import UpdateForm, CustomStringField -from .utils import get_email_domain def username_validation(form, field): @@ -57,19 +57,20 @@ class ExtendedEmail(Email): because they make our email sending service to fail """ + EMAIL_PATTERN = regex.compile( + r"""(?i)^[\x60#&*\/=?^{!}~'_\p{L}0-9\-\+]+ + (\.[\x60#&*\/=?^{!}~'_\p{L}0-9\-\+]+)*\.?@ + ([_a-z0-9-]+(\.[_a-z0-9-]+)*\.) + [a-z0-9-]*[a-z0-9]{2,}$""", + regex.VERBOSE, + ) + def __call__(self, form, field): super().__call__(form, field) - if re.search(r"[|'—]", field.data): - raise ValidationError( - f"Email address '{field.data}' contains an invalid character." - ) - - domain = get_email_domain(field.data) - if not domain.isascii(): - raise ValidationError( - f"Email address '{field.data}' contains non-ASCII characters in the domain part." - ) + value = field.data.strip() + if not self.EMAIL_PATTERN.match(value): + raise ValidationError(f"Email address '{value}' is invalid.") class PasswordValidator: From 40ce2d84f69349208a80899f78accb8f2d667821 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Wed, 5 Nov 2025 13:37:01 +0100 Subject: [PATCH 4/8] fix auth tests --- server/Pipfile | 2 +- server/mergin/tests/test_auth.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/Pipfile b/server/Pipfile index 4d19a818..8f724a68 100644 --- a/server/Pipfile +++ b/server/Pipfile @@ -41,9 +41,9 @@ importlib-metadata = "==8.4.0" # https://github.com/pallets/flask/issues/4502 typing_extensions = "==4.12.2" python-magic = "==0.4.27" click = "==8.2.0" +regex = "==2025.11.3" # requirements for development on windows colorama = "==0.4.5" -regex = "2025.11.3" [dev-packages] pytest = "==8.3.2" diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index b953577d..1afd5cc8 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -937,12 +937,12 @@ def test_server_usage(client): ("日人日本人", True), # non-ascii character ("usér", True), # non-ascii character ("user\\", False), # disallowed character - ("user\260", True), # non-ascii character (°) + ("user\260", False), # not letter character (°) ("user|", False), # vertical bar ("us er", False), # space in the middle ("us,er", False), # comma ("us—er", False), # dash - ("us'er", False), # apostrophe + ("us´er", False), # acute accent (" user", True), # starting with space (will be stripped) ("us.er", True), # dot in the middle (".user", False), # starting with dot From d4f267371a4d89ac6992c91186e028e284482c03 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Thu, 6 Nov 2025 12:49:20 +0100 Subject: [PATCH 5/8] Revert "use supplied regex" This reverts commit 178020c28f3ad49b84c47a3827e4d57ad4cffb2a. --- server/Pipfile.lock | 122 ------------------------------------ server/mergin/auth/forms.py | 25 ++++---- 2 files changed, 12 insertions(+), 135 deletions(-) diff --git a/server/Pipfile.lock b/server/Pipfile.lock index aa7566b8..ac41e613 100644 --- a/server/Pipfile.lock +++ b/server/Pipfile.lock @@ -1028,128 +1028,6 @@ "markers": "python_version >= '3.9'", "version": "==0.36.2" }, - "regex": { - "hashes": [ - "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", - "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", - "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", - "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", - "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", - "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", - "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", - "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", - "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", - "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", - "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", - "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", - "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", - "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", - "sha256:22dd622a402aad4558277305350699b2be14bc59f64d64ae1d928ce7d072dced", - "sha256:22e7d1cdfa88ef33a2ae6aa0d707f9255eb286ffbd90045f1088246833223aee", - "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", - "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", - "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", - "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", - "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", - "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", - "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", - "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", - "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", - "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", - "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", - "sha256:3bf28b1873a8af8bbb58c26cc56ea6e534d80053b41fb511a35795b6de507e6a", - "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", - "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", - "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", - "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", - "sha256:43b4fb020e779ca81c1b5255015fe2b82816c76ec982354534ad9ec09ad7c9e3", - "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", - "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", - "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", - "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", - "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", - "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", - "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", - "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", - "sha256:51c1c1847128238f54930edb8805b660305dca164645a9fd29243f5610beea34", - "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", - "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", - "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", - "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", - "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", - "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", - "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", - "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", - "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", - "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", - "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", - "sha256:732aea6de26051af97b94bc98ed86448821f839d058e5d259c72bf6d73ad0fc0", - "sha256:74d04244852ff73b32eeede4f76f51c5bcf44bc3c207bc3e6cf1c5c45b890708", - "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", - "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", - "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", - "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", - "sha256:7a50cd39f73faa34ec18d6720ee25ef10c4c1839514186fcda658a06c06057a2", - "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", - "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", - "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", - "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", - "sha256:81519e25707fc076978c6143b81ea3dc853f176895af05bf7ec51effe818aeec", - "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", - "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", - "sha256:856a25c73b697f2ce2a24e7968285579e62577a048526161a2c0f53090bea9f9", - "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", - "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", - "sha256:8a3d571bd95fade53c86c0517f859477ff3a93c3fde10c9e669086f038e0f207", - "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", - "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", - "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", - "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", - "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", - "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", - "sha256:a295ca2bba5c1c885826ce3125fa0b9f702a1be547d821c01d65f199e10c01e2", - "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", - "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", - "sha256:b4774ff32f18e0504bfc4e59a3e71e18d83bc1e171a3c8ed75013958a03b2f14", - "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", - "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", - "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", - "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", - "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", - "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", - "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", - "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", - "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", - "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", - "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", - "sha256:cc4076a5b4f36d849fd709284b4a3b112326652f3b0466f04002a6c15a0c96c1", - "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", - "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", - "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", - "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", - "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", - "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", - "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", - "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", - "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", - "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", - "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", - "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", - "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", - "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", - "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", - "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", - "sha256:f3b5a391c7597ffa96b41bd5cbd2ed0305f515fcbb367dfa72735679d5502364", - "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", - "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", - "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", - "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", - "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==2025.11.3" - }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index e5339b77..884d0dad 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -1,8 +1,7 @@ # Copyright (C) Lutra Consulting Limited # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial - -import regex +import re import safe from flask_wtf import FlaskForm from sqlalchemy import func @@ -18,6 +17,7 @@ from .models import MAX_USERNAME_LENGTH, User from ..app import UpdateForm, CustomStringField +from .utils import get_email_domain def username_validation(form, field): @@ -57,20 +57,19 @@ class ExtendedEmail(Email): because they make our email sending service to fail """ - EMAIL_PATTERN = regex.compile( - r"""(?i)^[\x60#&*\/=?^{!}~'_\p{L}0-9\-\+]+ - (\.[\x60#&*\/=?^{!}~'_\p{L}0-9\-\+]+)*\.?@ - ([_a-z0-9-]+(\.[_a-z0-9-]+)*\.) - [a-z0-9-]*[a-z0-9]{2,}$""", - regex.VERBOSE, - ) - def __call__(self, form, field): super().__call__(form, field) - value = field.data.strip() - if not self.EMAIL_PATTERN.match(value): - raise ValidationError(f"Email address '{value}' is invalid.") + if re.search(r"[|'—]", field.data): + raise ValidationError( + f"Email address '{field.data}' contains an invalid character." + ) + + domain = get_email_domain(field.data) + if not domain.isascii(): + raise ValidationError( + f"Email address '{field.data}' contains non-ASCII characters in the domain part." + ) class PasswordValidator: From ea719023ca138acc029a44fd6e7ccea3b2b3ab7f Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 7 Nov 2025 07:33:11 +0100 Subject: [PATCH 6/8] rm get_email_domain in auth utils --- server/mergin/auth/utils.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 server/mergin/auth/utils.py diff --git a/server/mergin/auth/utils.py b/server/mergin/auth/utils.py deleted file mode 100644 index b81b2eca..00000000 --- a/server/mergin/auth/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (C) Lutra Consulting Limited -# -# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial - -from email_validator import validate_email, EmailNotValidError - - -def get_email_domain(email: str) -> str | None: - try: - result = validate_email(email, check_deliverability=False) - return result.domain - except EmailNotValidError: - return From d4ac7ed2ee6b91db02082eaffc8cfd8d090e8f4b Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 7 Nov 2025 07:33:37 +0100 Subject: [PATCH 7/8] use re lib to validate emails --- server/mergin/auth/forms.py | 23 +++++++++-------------- server/mergin/tests/test_auth.py | 7 ++++++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index 884d0dad..f7403bb8 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -1,6 +1,7 @@ # Copyright (C) Lutra Consulting Limited # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial + import re import safe from flask_wtf import FlaskForm @@ -17,7 +18,6 @@ from .models import MAX_USERNAME_LENGTH, User from ..app import UpdateForm, CustomStringField -from .utils import get_email_domain def username_validation(form, field): @@ -51,25 +51,20 @@ class ExtendedEmail(Email): 3, multiple @ symbols, 4, leading, trailing, or consecutive dots in the local part, 5, invalid domain part - missing top level domain (user@example), consecutive dots, - Custom check for - - additional invalid characters disallows |'— - - non-ASCII characters in the domain part - because they make our email sending service to fail + The extended validation checks email addresses using the regex provided by Brevo, + so that we stay consistent with their validation rules and avoid API failures. """ def __call__(self, form, field): super().__call__(form, field) - if re.search(r"[|'—]", field.data): - raise ValidationError( - f"Email address '{field.data}' contains an invalid character." - ) + email = field.data.strip() - domain = get_email_domain(field.data) - if not domain.isascii(): - raise ValidationError( - f"Email address '{field.data}' contains non-ASCII characters in the domain part." - ) + pattern = r"^[\x60#&*\/=?^{!}~'+\w-]+(\.[\x60#&*\/=?^{!}~'+\w-]+)*\.?@([_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*\.)[a-zA-Z0-9-]*[a-zA-Z0-9]{2,}$" + email_regexp = re.compile(pattern, re.IGNORECASE) + + if not email_regexp.match(email): + raise ValidationError(f"Email address '{email}' is invalid.") class PasswordValidator: diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index 1afd5cc8..69ba37c6 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -125,7 +125,11 @@ def test_logout(client): 400, ), # tests with upper case, but email already exists (" mergin@mergin.com ", "#pwd123", 400), # invalid password - ("verylonglonglonglonglonglonglongemail@example.com", "#pwd1234", 201), + ( + "verylonglonglonglonglonglonglongemail@lutra-consulting.co.uk", + "#pwd1234", + 201, + ), # long local part, second-level domain, dash in domain ("us.er@mergin.com", "#pwd1234", 201), # dot is allowed ("us er@mergin.com", "#pwd1234", 400), # space is disallowed ("test@gmaiñ.com", "#pwd1234", 400), # non-ASCII character in the domain @@ -946,6 +950,7 @@ def test_server_usage(client): (" user", True), # starting with space (will be stripped) ("us.er", True), # dot in the middle (".user", False), # starting with dot + ("us-er", True), # hyphen ] From caed4dff69e2feb985850d5ecd49671e57bbd490 Mon Sep 17 00:00:00 2001 From: Herman Snevajs Date: Fri, 7 Nov 2025 12:22:07 +0100 Subject: [PATCH 8/8] rm regex from pipfile --- server/Pipfile | 1 - 1 file changed, 1 deletion(-) diff --git a/server/Pipfile b/server/Pipfile index 8f724a68..3f9cb0a5 100644 --- a/server/Pipfile +++ b/server/Pipfile @@ -41,7 +41,6 @@ importlib-metadata = "==8.4.0" # https://github.com/pallets/flask/issues/4502 typing_extensions = "==4.12.2" python-magic = "==0.4.27" click = "==8.2.0" -regex = "==2025.11.3" # requirements for development on windows colorama = "==0.4.5"