Loading lib/galaxy/security/validate_user_input.py +43 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,10 @@ user inputs - so these methods do not need to be escaped. import logging import re from typing import Optional from typing import ( List, Optional, ) import dns.resolver from dns.exception import DNSException Loading Loading @@ -73,7 +76,9 @@ def validate_publicname_str(publicname): def validate_email(trans, email, user=None, check_dup=True, allow_empty=False, validate_domain=False): """ Validates the email format, also checks whether the domain is blocklisted in the disposable domains configuration. Validates the email format. Checks whether the domain is blocklisted in the disposable domains configuration. Checks whether the email address is banned. """ if (user and user.email == email) or (email == "" and allow_empty): return "" Loading @@ -82,6 +87,10 @@ def validate_email(trans, email, user=None, check_dup=True, allow_empty=False, v domain = extract_domain(email) message = validate_email_domain_name(domain) if not message: if is_email_banned(email, trans.app.config.email_ban_file): message = "This email address has been banned." stmt = select(trans.app.model.User).filter(func.lower(trans.app.model.User.email) == email.lower()).limit(1) if not message and check_dup and trans.sa_session.scalars(stmt).first(): message = f"User with email '{email}' already exists." Loading Loading @@ -164,3 +173,35 @@ def validate_preferred_object_store_id( trans, object_store: ObjectStore, preferred_object_store_id: Optional[str] ) -> str: return object_store.validate_selected_object_store_id(trans.user, preferred_object_store_id) or "" def is_email_banned(email: str, filepath: Optional[str]) -> bool: if not filepath: return False email = _make_canonical_email(email) banned_emails = _read_email_ban_list(filepath) for address in banned_emails: if email == _make_canonical_email(address): return True return False def _make_canonical_email(email: str) -> str: """ Transform to canonical representation: - lowercase - gmail: drop periods in local-part - gmail: drop plus suffixes in local-part """ email = email.lower() localpart, domain = email.split("@") if domain == "gmail.com": localpart = localpart.replace(".", "") if localpart.find("+") > -1: localpart = localpart[: localpart.index("+")] return f"{localpart}@{domain}" def _read_email_ban_list(filepath: str) -> List[str]: with open(filepath) as f: return [line.strip() for line in f if not line.startswith("#")] test/unit/data/security/test_validate_user_input.py +16 −0 Original line number Diff line number Diff line from galaxy.security import validate_user_input from galaxy.security.validate_user_input import ( extract_domain, is_email_banned, validate_email_domain_name, validate_email_str, validate_publicname_str, Loading Loading @@ -46,3 +48,17 @@ def test_validate_email_str(): assert validate_email_str('"i-like-to-break-email-valid@tors"@foo.com') != "" too_long_email = "N" * 255 + "@foo.com" assert validate_email_str(too_long_email) != "" def test_is_email_banned(monkeypatch): mock_ban_list = ["ab@foo.com", "ab@gmail.com", "Not.Canonical+email+gmail+address@gmail.com"] monkeypatch.setattr(validate_user_input, "_read_email_ban_list", lambda a: mock_ban_list) assert is_email_banned("a.b@gmail.com", "_") assert is_email_banned("ab@gmail.com", "_") assert is_email_banned("a.b+c@gmail.com", "_") assert is_email_banned("Ab@foo.com", "_") assert is_email_banned("a.b.+c.d@gmail.com", "_") assert is_email_banned("not.canonical@gmail.com", "_") assert not is_email_banned("ab@not-gmail.com", "_") assert not is_email_banned("a.b+c@not-gmail.com", "_") Loading
lib/galaxy/security/validate_user_input.py +43 −2 Original line number Diff line number Diff line Loading @@ -7,7 +7,10 @@ user inputs - so these methods do not need to be escaped. import logging import re from typing import Optional from typing import ( List, Optional, ) import dns.resolver from dns.exception import DNSException Loading Loading @@ -73,7 +76,9 @@ def validate_publicname_str(publicname): def validate_email(trans, email, user=None, check_dup=True, allow_empty=False, validate_domain=False): """ Validates the email format, also checks whether the domain is blocklisted in the disposable domains configuration. Validates the email format. Checks whether the domain is blocklisted in the disposable domains configuration. Checks whether the email address is banned. """ if (user and user.email == email) or (email == "" and allow_empty): return "" Loading @@ -82,6 +87,10 @@ def validate_email(trans, email, user=None, check_dup=True, allow_empty=False, v domain = extract_domain(email) message = validate_email_domain_name(domain) if not message: if is_email_banned(email, trans.app.config.email_ban_file): message = "This email address has been banned." stmt = select(trans.app.model.User).filter(func.lower(trans.app.model.User.email) == email.lower()).limit(1) if not message and check_dup and trans.sa_session.scalars(stmt).first(): message = f"User with email '{email}' already exists." Loading Loading @@ -164,3 +173,35 @@ def validate_preferred_object_store_id( trans, object_store: ObjectStore, preferred_object_store_id: Optional[str] ) -> str: return object_store.validate_selected_object_store_id(trans.user, preferred_object_store_id) or "" def is_email_banned(email: str, filepath: Optional[str]) -> bool: if not filepath: return False email = _make_canonical_email(email) banned_emails = _read_email_ban_list(filepath) for address in banned_emails: if email == _make_canonical_email(address): return True return False def _make_canonical_email(email: str) -> str: """ Transform to canonical representation: - lowercase - gmail: drop periods in local-part - gmail: drop plus suffixes in local-part """ email = email.lower() localpart, domain = email.split("@") if domain == "gmail.com": localpart = localpart.replace(".", "") if localpart.find("+") > -1: localpart = localpart[: localpart.index("+")] return f"{localpart}@{domain}" def _read_email_ban_list(filepath: str) -> List[str]: with open(filepath) as f: return [line.strip() for line in f if not line.startswith("#")]
test/unit/data/security/test_validate_user_input.py +16 −0 Original line number Diff line number Diff line from galaxy.security import validate_user_input from galaxy.security.validate_user_input import ( extract_domain, is_email_banned, validate_email_domain_name, validate_email_str, validate_publicname_str, Loading Loading @@ -46,3 +48,17 @@ def test_validate_email_str(): assert validate_email_str('"i-like-to-break-email-valid@tors"@foo.com') != "" too_long_email = "N" * 255 + "@foo.com" assert validate_email_str(too_long_email) != "" def test_is_email_banned(monkeypatch): mock_ban_list = ["ab@foo.com", "ab@gmail.com", "Not.Canonical+email+gmail+address@gmail.com"] monkeypatch.setattr(validate_user_input, "_read_email_ban_list", lambda a: mock_ban_list) assert is_email_banned("a.b@gmail.com", "_") assert is_email_banned("ab@gmail.com", "_") assert is_email_banned("a.b+c@gmail.com", "_") assert is_email_banned("Ab@foo.com", "_") assert is_email_banned("a.b.+c.d@gmail.com", "_") assert is_email_banned("not.canonical@gmail.com", "_") assert not is_email_banned("ab@not-gmail.com", "_") assert not is_email_banned("a.b+c@not-gmail.com", "_")