From 56c5942259a94d71ce5d262d396eb57759edf941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Skaza?= Date: Wed, 26 Jul 2023 21:36:55 +0200 Subject: [PATCH] Add email whitelist wildcard (#2375) * add wildcard for email whitelisting --------- Co-authored-by: Kevin Chung --- CTFd/forms/config.py | 2 +- CTFd/utils/email/__init__.py | 22 +++++++++- tests/utils/test_email.py | 80 ++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/CTFd/forms/config.py b/CTFd/forms/config.py index 3a051670..ade6944c 100644 --- a/CTFd/forms/config.py +++ b/CTFd/forms/config.py @@ -31,7 +31,7 @@ class ResetInstanceForm(BaseForm): class AccountSettingsForm(BaseForm): domain_whitelist = StringField( "Account Email Whitelist", - description="Comma-seperated email domains which users can register under (e.g. ctfd.io, gmail.com, yahoo.com)", + description="Comma-seperated email domains which users can register under (e.g. ctfd.io, example.com, *.example.com)", ) team_creation = SelectField( "Team Creation", diff --git a/CTFd/utils/email/__init__.py b/CTFd/utils/email/__init__.py index 9a2fba21..08d7abf7 100644 --- a/CTFd/utils/email/__init__.py +++ b/CTFd/utils/email/__init__.py @@ -137,8 +137,26 @@ def user_created_notification(addr, name, password): def check_email_is_whitelisted(email_address): local_id, _, domain = email_address.partition("@") domain_whitelist = get_config("domain_whitelist") + if domain_whitelist: domain_whitelist = [d.strip() for d in domain_whitelist.split(",")] - if domain not in domain_whitelist: - return False + + for allowed_domain in domain_whitelist: + if allowed_domain.startswith("*."): + # domains should never container the "*" char + if "*" in domain: + return False + + # Handle wildcard domain case + suffix = allowed_domain[1:] # Remove the "*" prefix + if domain.endswith(suffix): + return True + + elif domain == allowed_domain: + return True + + # whitelist is specified but the email doesn't match any domains + return False + + # whitelist is not specified - allow all emails return True diff --git a/tests/utils/test_email.py b/tests/utils/test_email.py index 327c0ee5..4036da86 100644 --- a/tests/utils/test_email.py +++ b/tests/utils/test_email.py @@ -6,6 +6,7 @@ from freezegun import freeze_time from CTFd.utils import get_config, set_config from CTFd.utils.email import ( + check_email_is_whitelisted, sendmail, successful_registration_notification, verify_email_address, @@ -239,3 +240,82 @@ def test_successful_registration_email(mock_smtp): email_msg ) destroy_ctfd(app) + + +def test_email_whitelist(): + app = create_ctfd() + with app.app_context(): + set_config("domain_whitelist", "example.com") + test_cases_specific_domain = [ + ("john.doe@example.com", True), + ("john.doe@ext.example.com", False), + ("john.doe@example.io", False), + ("john.doe@example.co", False), + ("john.doe@ample.com", False), + ("john.doe@exexample.com", False), + ] + + for case in test_cases_specific_domain: + email, expected = case + assert check_email_is_whitelisted(email) is expected + + set_config("domain_whitelist", "*.example.com") + test_cases_wildcard_domain = [ + ("john.doe@ext.example.com", True), + ("john.doe@.example.com", True), # this is expected behaviour + ("john.doe@example.com", False), + ("john.doe@example.io", False), + ("john.doe@example.co", False), + ("john.doe@ample.com", False), + ("john.doe@exexample.com", False), + ("john.doe@*example.com", False), + ("john.doe@*.example.com", False), + ] + + for case in test_cases_wildcard_domain: + email, expected = case + assert check_email_is_whitelisted(email) is expected + + set_config("domain_whitelist", "example.com, *.example.com") + test_cases_combined_domain = [ + ("john.doe@example.com", True), + ("john.doe@ext.example.com", True), + ("john.doe@.example.com", True), # this is expected behaviour + ("john.doe@example.io", False), + ("john.doe@example.co", False), + ("john.doe@gmail.com", False), + ("john.doe@ample.com", False), + ("john.doe@exexample.com", False), + ("john.doe@*example.com", False), + ("john.doe@*.example.com", False), + ] + + for case in test_cases_combined_domain: + email, expected = case + assert check_email_is_whitelisted(email) is expected + + set_config("domain_whitelist", "example.com, uni.acme.com, *.edu, *.edu.de") + + test_cases_multiple_combined_domains = [ + ("john.doe@example.com", True), + ("john.doe@uni.acme.com", True), + ("john.doe@uni.edu", True), + ("john.doe@cs.uni.edu", True), + ("john.doe@mail.cs.uni.edu", True), + ("john.doe@uni.edu.de", True), + ("john.doe@cs.uni.edu.de", True), + ("john.doe@mail.cs.uni.edu.de", True), + ("john.doe@gmail.com", False), + ("john.doe@ample.com", False), + ("john.doe@example1.com", False), + ("john.doe@1example.com", False), + ("john.doe@ext.example.com", False), + ("john.doe@cs.acme.com", False), + ("john.doe@edu.com", False), + ("john.doe@mail.uni.acme.com", False), + ("john.doe@edu", False), + ] + + for case in test_cases_multiple_combined_domains: + email, expected = case + assert check_email_is_whitelisted(email) is expected