aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkj_sh6042026-04-03 02:42:36 -0400
committerkj_sh6042026-04-03 02:42:36 -0400
commit97d55ccdc97ef7b0cc28ded5ebbb46c1879291ce (patch)
tree51eb613c9a8b8bbff9f78940b70dea65817464a4
parentf22c51507eb59cf843f1baca2ad7626fd351f33d (diff)
refactor: better email validation
-rw-r--r--auth_backend.py31
-rw-r--r--requirements.txt3
-rw-r--r--shim_app.py10
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>