add better mail security

This commit is contained in:
rnsrk 2026-01-19 08:57:23 +01:00
parent a7585e1bc9
commit f670bec1b1
63 changed files with 2569 additions and 491 deletions

View file

@ -242,6 +242,7 @@ function user_login($user, $pass, $extra = null){
return false;
}
$row['attributes'] = json_decode($row['attributes'], true);
switch ($row['authsource']) {
case 'keycloak':
// user authsource is keycloak, try using via rest flow
@ -351,6 +352,11 @@ function user_login($user, $pass, $extra = null){
}
// verify password
if (verify_hash($row['password'], $pass) !== false) {
if (intval($row['attributes']['force_pw_update']) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
@ -471,6 +477,9 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
}
return false;
}
if (!$iam_provider) {
return false;
}
// get access_token for service account of mailcow client
$admin_token = identity_provider("get-keycloak-admin-token");
@ -540,6 +549,17 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
return 'user';
}
// check if login provisioning is enabled before creating user
if (!$iam_settings['login_provisioning']){
if (!$is_internal){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
}
return false;
}
// check if matching attribute exist
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
if (!empty($iam_settings['default_template'])) {
@ -653,10 +673,21 @@ function ldap_mbox_login($user, $pass, $extra = null){
return 'user';
}
// check if login provisioning is enabled before creating user
if (!$iam_settings['login_provisioning']){
if (!$is_internal){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
}
return false;
}
// check if matching attribute exist
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
if (!empty($iam_settings['default_tempalte'])) {
$mbox_template = $iam_settings['default_tempalte'];
if (!empty($iam_settings['default_template'])) {
$mbox_template = $iam_settings['default_template'];
} else {
$_SESSION['return'][] = array(
'type' => 'danger',

View file

@ -204,6 +204,35 @@ function customize($_action, $_item, $_data = null) {
'msg' => 'ip_check_opt_in_modified'
);
break;
case 'custom_login':
$hide_user_quicklink = ($_data['hide_user_quicklink'] == "1") ? 1 : 0;
$hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0;
$hide_admin_quicklink = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0;
$force_sso = ($_data['force_sso'] == "1") ? 1 : 0;
$custom_login = array(
"hide_user_quicklink" => $hide_user_quicklink,
"hide_domainadmin_quicklink" => $hide_domainadmin_quicklink,
"hide_admin_quicklink" => $hide_admin_quicklink,
"force_sso" => $force_sso,
);
try {
$redis->set('CUSTOM_LOGIN', json_encode($custom_login));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'custom_login_modified'
);
break;
}
break;
case 'delete':
@ -357,6 +386,20 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'custom_login':
try {
$custom_login = $redis->get('CUSTOM_LOGIN');
return $custom_login ? json_decode($custom_login, true) : array();
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
break;
}
break;
}

View file

@ -1001,6 +1001,7 @@ function edit_user_account($_data) {
':password_hashed' => $password_hashed,
':username' => $username
));
$_SESSION['pending_pw_update'] = false;
update_sogo_static_view();
}
@ -2286,12 +2287,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $row){
switch ($row["key"]) {
case "redirect_url_extra":
case "mappers":
case "templates":
$settings[$row["key"]] = json_decode($row["value"]);
break;
case "use_ssl":
case "use_tls":
case "login_provisioning":
case "ignore_ssl_errors":
$settings[$row["key"]] = boolval($row["value"]);
break;
@ -2300,6 +2303,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
break;
}
}
// set login_provisioning if not exists
if (!array_key_exists('login_provisioning', $settings)) {
$settings['login_provisioning'] = 1;
}
// return default client_scopes for generic-oidc if none is set
if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){
$settings["client_scopes"] = "openid profile email mailcow_template";
@ -2364,7 +2371,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
return false;
}
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
$_data['login_provisioning'] = isset($_data['login_provisioning']) ? boolval($_data['login_provisioning']) : false;
switch ($_data['authsource']) {
case "keycloak":
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
@ -2373,14 +2381,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error');
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning');
break;
case "generic-oidc":
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
$_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
$_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
$_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email mailcow_template";
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error');
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error', 'login_provisioning');
break;
case "ldap":
$_data['host'] = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : "";
@ -2394,7 +2402,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error');
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning');
break;
}
@ -2418,6 +2426,18 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
}
$pdo->commit();
// add redirect_url_extra
if (isset($_data['redirect_url_extra'])){
$_data['redirect_url_extra'] = (!is_array($_data['redirect_url_extra'])) ? array($_data['redirect_url_extra']) : $_data['redirect_url_extra'];
$redirect_url_extra = array_filter($_data['redirect_url_extra']);
$redirect_url_extra = json_encode($redirect_url_extra);
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('redirect_url_extra', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
$stmt->bindParam(':value', $redirect_url_extra);
$stmt->execute();
}
// add default template
if (isset($_data['default_template'])) {
$_data['default_template'] = (empty($_data['default_template'])) ? "" : $_data['default_template'];
@ -2752,6 +2772,16 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
return true;
}
// user doesn't exist, check if login provisioning is enabled
if (!$iam_settings['login_provisioning']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
return false;
}
if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){
if (!empty($iam_settings['default_template'])) {
$mbox_template = $iam_settings['default_template'];
@ -2851,7 +2881,19 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
case "get-redirect":
if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
return false;
$authUrl = $iam_provider->getAuthorizationUrl();
$options = [];
if (isset($iam_settings['redirect_url_extra'])) {
// check if the current domain is used in an extra redirect URL
$targetDomain = strtolower($_SERVER['HTTP_HOST']);
foreach ($iam_settings['redirect_url_extra'] as $testUrl) {
$testUrlParsed = parse_url($testUrl);
if (isset($testUrlParsed['host']) && strtolower($testUrlParsed['host']) == $targetDomain) {
$options['redirect_uri'] = $testUrl;
break;
}
}
}
$authUrl = $iam_provider->getAuthorizationUrl($options);
$_SESSION['oauth2state'] = $iam_provider->getState();
return $authUrl;
break;

View file

@ -169,7 +169,7 @@ function quarantine($_action, $_data = null) {
}
}
elseif ($release_format == 'raw') {
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']);
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
@ -464,7 +464,7 @@ function quarantine($_action, $_data = null) {
}
}
elseif ($release_format == 'raw') {
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']);
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),

View file

@ -76,7 +76,10 @@ if (isset($_POST["verify_tfa_login"])) {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
die();
} else {
@ -139,7 +142,10 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$user_details = mailbox("get", "mailbox_details", $login_user);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/{$login_user}");
die();
} else {