diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 90c830b69..a08e39b55 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -633,6 +633,18 @@ function logger($_data = false) { return true; } } +function is_local_mailcow_domain($domain) { + // True if domain is a locally managed, active primary or alias domain + global $pdo; + $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); + if (empty($domain)) { + return false; + } + $stmt = $pdo->prepare("SELECT 1 FROM `domain` WHERE `domain` = :d AND `active` = 1 + UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d2 AND `active` = 1 LIMIT 1"); + $stmt->execute(array(':d' => $domain, ':d2' => $domain)); + return (bool)$stmt->fetchColumn(); +} function hasDomainAccess($username, $role, $domain) { global $pdo; if (empty($domain) || !is_valid_domain_name($domain)) { diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index acbbb94d4..37ccbd85c 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -750,6 +750,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); $goto_local_part = strstr($goto, '@', true); $goto = $goto_local_part.'@'.$goto_domain; + // Deny external goto domains: global switch (all roles) overrides the per-DA ACL + if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true || + (isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) && + !is_local_mailcow_domain($goto_domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('external_goto_denied', htmlspecialchars($goto)) + ); + unset($gotos[$i]); + continue; + } $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `username`= :goto"); @@ -2715,6 +2727,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { unset($gotos[$i]); continue; } + // Deny external goto domains: global switch (all roles) overrides the per-DA ACL + $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); + if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true || + (isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) && + !is_local_mailcow_domain($goto_domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('external_goto_denied', htmlspecialchars($goto)) + ); + unset($gotos[$i]); + continue; + } if ($goto == $address) { $_SESSION['return'][] = array( 'type' => 'danger', diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 23aa5e77f..f6b0cd792 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -4,7 +4,7 @@ function init_db_schema() try { global $pdo; - $db_version = "19022026_1220"; + $db_version = "18082026_1200"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -719,7 +719,8 @@ function init_db_schema() "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" + "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'", + "alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'" ), "keys" => array( "primary" => array( diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index cec402a96..2265c5e1c 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -246,6 +246,10 @@ $PW_RESET_TOKEN_LIMIT = 3; // Maximum time in minutes a password reset token is valid $PW_RESET_TOKEN_LIFETIME = 15; +// Globally forbid aliases with external (non-local) goto domains for ALL roles (incl. admins), +// overriding the per-domain-admin da_acl. false = defer to da_acl. +$ALIAS_DISABLE_EXTERNAL_DOMAINS = false; + // UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins // true = required // false = preferred diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 28ad0d72b..938f9b929 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -1,6 +1,7 @@ { "acl": { "alias_domains": "Alias-Domains hinzufügen", + "alias_external_goto": "Aliase mit externen Ziel-Domains erlauben", "app_passwds": "App-Passwörter verwalten", "bcc_maps": "BCC-Maps", "delimiter_action": "Delimiter-Aktionen (tags)", @@ -443,6 +444,7 @@ "domain_not_found": "Domain %s nicht gefunden", "domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein", "extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen", + "external_goto_denied": "Externe Ziel-Adresse %s ist nicht erlaubt", "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig", "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain", "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index b8a11e5a5..c958e68eb 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -1,6 +1,7 @@ { "acl": { "alias_domains": "Add alias domains", + "alias_external_goto": "Allow aliases with external goto domains", "app_passwds": "Manage app passwords", "bcc_maps": "BCC maps", "delimiter_action": "Delimiter action", @@ -444,6 +445,7 @@ "domain_not_found": "Domain %s not found", "domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB", "extended_sender_acl_denied": "missing ACL to set external sender addresses", + "external_goto_denied": "External goto address %s is not allowed", "extra_acl_invalid": "External sender address \"%s\" is invalid", "extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain", "fido2_verification_failed": "FIDO2 verification failed: %s",