diff options
| author | kj_sh604 | 2026-04-03 02:42:36 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-04-03 02:42:36 -0400 |
| commit | 97d55ccdc97ef7b0cc28ded5ebbb46c1879291ce (patch) | |
| tree | 51eb613c9a8b8bbff9f78940b70dea65817464a4 | |
| parent | f22c51507eb59cf843f1baca2ad7626fd351f33d (diff) | |
refactor: better email validation
| -rw-r--r-- | auth_backend.py | 31 | ||||
| -rw-r--r-- | requirements.txt | 3 | ||||
| -rw-r--r-- | shim_app.py | 10 |
3 files changed, 20 insertions, 24 deletions
diff --git a/auth_backend.py b/auth_backend.py index 00a5dbc..817f8d4 100644 --- a/auth_backend.py +++ b/auth_backend.py @@ -9,13 +9,13 @@ import uuid from pathlib import Path from typing import Callable, Optional, Protocol +from email_validator import EmailNotValidError, validate_email + # fixed challenge text used for passphrase verification. # we store encrypted challenge output instead of storing passwords. AUTH_CHALLENGE = "SHIM_AUTH_VALID" HANDLE_RE = re.compile(r"^[a-z0-9_.-]{2,64}$") -EMAIL_LOCAL_RE = re.compile(r"^[a-z0-9!#$%&'*+/=?^_`{|}~.-]{1,64}$") -EMAIL_DOMAIN_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$") UUID_RE = re.compile( r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" ) @@ -54,25 +54,18 @@ def normalize_uuid(value: str) -> Optional[str]: def is_valid_email_username(value: str) -> bool: if len(value) > 254: return False - if "@" not in value or value.count("@") != 1: - return False - - local_part, domain = value.split("@", 1) - if not local_part or not domain: - return False - if local_part.startswith(".") or local_part.endswith(".") or ".." in local_part: - return False - if not EMAIL_LOCAL_RE.fullmatch(local_part): + try: + validated = validate_email( + value, + check_deliverability=False, + allow_smtputf8=False, + allow_display_name=False, + ) + except EmailNotValidError: return False - labels = domain.split(".") - if len(labels) < 2: - return False - if any(not label for label in labels): - return False - if len(labels[-1]) < 2: - return False - return all(EMAIL_DOMAIN_LABEL_RE.fullmatch(label) for label in labels) + # keep stored usernames predictable - only accept already normalized forms. + return validated.normalized == value def looks_like_python_script(path: Path) -> bool: diff --git a/requirements.txt b/requirements.txt index cc83c7b..6a98c4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ blinker==1.9.0 click==8.3.1 +dnspython==2.8.0 +email_validator==2.2.0 Flask==3.1.3 +idna==3.11 itsdangerous==2.2.0 Jinja2==3.1.6 MarkupSafe==3.0.3 diff --git a/shim_app.py b/shim_app.py index 6137f15..4cc3dec 100644 --- a/shim_app.py +++ b/shim_app.py @@ -171,7 +171,7 @@ SHELL_TEMPLATE = """<!doctype html> <button type="submit">logout</button> </form> {% else %} - <a href="https://youtu.be/XGxIE1hr0w4" target="_blank">🦄</a> + <a href="https://youtu.be/XGxIE1hr0w4" target="_blank">🧷</a> {% endif %} </nav> </header> @@ -196,8 +196,8 @@ SETUP_BODY_TEMPLATE = """ <p>first startup detected. <br><br> create the initial admin account to continue.</p> <form method="post" action="{{ url_for('setup_submit') }}" class="stack" autocomplete="off"> <label> - username or email - <input name="username" type="text" required minlength="2" maxlength="254" placeholder="admin username" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> + username <strong>(admin)</strong> + <input name="username" type="text" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> password @@ -218,7 +218,7 @@ LOGIN_BODY_TEMPLATE = """ <p>a no-frills, "hackfoo" static site hosting solution</p> <form method="post" action="{{ url_for('login_submit') }}" class="stack" autocomplete="off"> <label> - username or email + username <input name="username" type="text" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> @@ -322,7 +322,7 @@ DASHBOARD_BODY_TEMPLATE = """ <h2>create user</h2> <form method="post" action="{{ url_for('admin_create_user') }}" class="stack" autocomplete="off"> <label> - username or email + username <input type="text" name="username" required minlength="2" maxlength="254" autocomplete="new-password" autocapitalize="none" autocorrect="off" spellcheck="false" data-lpignore="true" data-unlock-readonly="1" readonly> </label> <label> |
