diff --git a/apps/labrinth/src/env.rs b/apps/labrinth/src/env.rs index 8cd0984819..e403be77ca 100644 --- a/apps/labrinth/src/env.rs +++ b/apps/labrinth/src/env.rs @@ -238,6 +238,8 @@ vars! { NEVERBOUNCE_API_KEY: String = ""; NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL; + EMAIL_DOMAIN_BLACKLIST: StringCsv = StringCsv(vec![]); + CLICKHOUSE_REPLICATED: bool = false; CLICKHOUSE_URL: String = "http://localhost:8123"; CLICKHOUSE_USER: String = "default"; diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index 637a0af346..4ccd811089 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -1816,7 +1816,39 @@ impl From for AccountRegisterFlow { } } +/// Blacklist entries are matched literally, unless they begin with `*.`, in +/// which case they match any subdomain of the remaining suffix. +fn is_blacklisted_domain(domain: &str) -> bool { + let domain = domain.to_ascii_lowercase(); + + ENV.EMAIL_DOMAIN_BLACKLIST.iter().any(|entry| { + let entry = entry.trim().to_ascii_lowercase(); + + match entry.strip_prefix("*.") { + Some(suffix) => domain + .strip_suffix(suffix) + .is_some_and(|subdomain| subdomain.ends_with('.')), + None => entry == domain, + } + }) +} + +fn ensure_email_domain_is_allowed(email: &str) -> Result<(), ApiError> { + let Some((_, domain)) = email.rsplit_once('@') else { + return Err(ApiError::Request(email_check_error_generic())); + }; + + if is_blacklisted_domain(domain) { + info!(email.domain = domain, "blacklisted email domain, denying"); + return Err(ApiError::Request(email_check_error_generic())); + } + + Ok(()) +} + async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> { + ensure_email_domain_is_allowed(email)?; + let result = check_email(email).await.map_err(ApiError::Request)?; if matches!(