From 4a03b5c1a17e83106bbb32732770db4e522bec2c Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Wed, 21 Oct 2026 15:19:21 +0000 Subject: [PATCH] Reserve privileged alias addresses for admins Any user could create aliases like postmaster@, webmaster@ or abuse@ on their domain. CAs use these addresses to validate domains, so a family account (or a compromised one) could obtain TLS certificates or catch abuse and system mails. - Only admins may create aliases with a reserved local part (admin, administrator, webmaster, hostmaster, postmaster, abuse, security, noc, root, mailer-daemon). The acting user counts, so admins can still assign such aliases to other users. - Escape the domains in the email regex, so "finn.st" no longer matches "finnXst". Co-Authored-By: Claude Opus 5.5 --- max/private_static/i18n/de.json | 3 ++- max/routes/alias/create.py | 6 ++++++ max/routes/check_email_mixin.py | 22 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/max/private_static/i18n/de.json b/max/private_static/i18n/de.json index 57a8e43..92f693f 100644 --- a/max/private_static/i18n/de.json +++ b/max/private_static/i18n/de.json @@ -65,5 +65,6 @@ "Used as an alias by {alias}.": "Word für den Alias {alias} verwendet", "Invalid domain": "Invalide Domain", "Can use different alias domains": "Kann unterschiedliche Aliasdomains benutzen", - "Can use different alias domains is not set": "Kann unterschiedliche Aliasdomains benutzen ist nicht gesetzt" + "Can use different alias domains is not set": "Kann unterschiedliche Aliasdomains benutzen ist nicht gesetzt", + "This address is reserved and can only be created by an admin.": "Diese Adresse ist reserviert und kann nur von einem Admin angelegt werden." } \ No newline at end of file diff --git a/max/routes/alias/create.py b/max/routes/alias/create.py index 71bb04b..2e7bd8a 100644 --- a/max/routes/alias/create.py +++ b/max/routes/alias/create.py @@ -42,6 +42,12 @@ class AliasCreate( full_email = f"{self.email}@{self.domain}" if not self.is_email_valid(full_email): error = True + elif not self.auth_user.is_admin and self.is_reserved_email(full_email): + flash( + t("This address is reserved and can only be created by an admin."), + category="error", + ) + error = True enabled = self.request.form.get("enabled") if enabled == "on": diff --git a/max/routes/check_email_mixin.py b/max/routes/check_email_mixin.py index aab694a..f1da315 100644 --- a/max/routes/check_email_mixin.py +++ b/max/routes/check_email_mixin.py @@ -5,14 +5,34 @@ from flask import flash from max.db import get_domains from max.translations import t +RESERVED_LOCAL_PARTS = frozenset( + ( + "admin", + "administrator", + "webmaster", + "hostmaster", + "postmaster", + "abuse", + "security", + "noc", + "root", + "mailer-daemon", + ) +) + class CheckEmailMixin: def is_email_valid(self, email: str): """Checks for the email being valid and that it has a valid domain.""" - regex = re.compile(f"^[a-zA-Z0-9+_-]+@({'|'.join(get_domains())})$") + domains = "|".join(map(re.escape, get_domains())) + regex = re.compile(f"^[a-zA-Z0-9+_-]+@({domains})$") match = regex.match(email) if not match: flash(t("The email is not valid"), category="error") return False return True + + def is_reserved_email(self, email: str): + local_part = email.split("@")[0] + return local_part.lower() in RESERVED_LOCAL_PARTS