add otp security check to critical settings

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2023-07-10 13:40:48 +02:00
parent 03b5a921ff
commit f396bd5184
20 changed files with 235 additions and 35 deletions

View File

@@ -107,7 +107,8 @@ return [
'varname' => 'enabled',
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'api_customer_default' => [
'label' => lng('serversettings.api_customer_default'),

View File

@@ -46,7 +46,8 @@ return [
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => '/usr/bin/nice -n 5 /usr/bin/php -q',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_crondreload' => [
'label' => lng('serversettings.system_crondreload'),
@@ -55,7 +56,8 @@ return [
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => '/etc/init.d/cron reload',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_cron_allowautoupdate' => [
'label' => lng('serversettings.system_cron_allowautoupdate'),
@@ -63,7 +65,8 @@ return [
'varname' => 'cron_allowautoupdate',
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
]
]
]

View File

@@ -308,7 +308,8 @@ return [
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => '/etc/init.d/apache2 reload',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_phpreload_command' => [
'label' => lng('serversettings.phpreload_command'),
@@ -320,7 +321,8 @@ return [
'save_method' => 'storeSettingField',
'websrv_avail' => [
'nginx'
]
],
'required_otp' => true
],
'system_nginx_php_backend' => [
'label' => lng('serversettings.nginx_php_backend'),

View File

@@ -157,7 +157,8 @@ return [
'string_type' => 'file',
'default' => '/root/.acme.sh/acme.sh',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'system_letsencryptacmeconf' => [
'label' => lng('serversettings.letsencryptacmeconf'),

View File

@@ -126,7 +126,8 @@ return [
'type' => 'textarea',
'default' => '',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'phpfpm_ini_values' => [
'label' => lng('phpfpm.ini_values'),
@@ -135,7 +136,8 @@ return [
'type' => 'textarea',
'default' => '',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'phpfpm_ini_admin_flags' => [
'label' => lng('phpfpm.ini_admin_flags'),
@@ -144,7 +146,8 @@ return [
'type' => 'textarea',
'default' => '',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'phpfpm_ini_admin_values' => [
'label' => lng('phpfpm.ini_admin_values'),
@@ -153,7 +156,8 @@ return [
'type' => 'textarea',
'default' => '',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
]
]
]

View File

@@ -80,7 +80,8 @@ return [
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => '/etc/init.d/bind9 reload',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_nameservers' => [
'label' => lng('serversettings.nameservers'),
@@ -111,7 +112,8 @@ return [
'string_delimiter' => ',',
'string_emptyallowed' => true,
'default' => '',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_powerdns_mode' => [
'label' => lng('serversettings.powerdns_mode'),

View File

@@ -137,7 +137,8 @@ return [
'type' => 'text',
'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i',
'default' => '/etc/init.d/dkim-filter restart',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
]
]
]

View File

@@ -37,7 +37,8 @@ return [
'varname' => 'unix_names',
'type' => 'checkbox',
'default' => true,
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_mailpwcleartext' => [
'label' => lng('serversettings.mailpwcleartext'),
@@ -46,7 +47,8 @@ return [
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'system_passwordcryptfunc' => [
'label' => lng('serversettings.passwordcryptfunc'),
@@ -59,7 +61,8 @@ return [
'getAvailablePasswordHashes'
],
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'system_allow_error_report_admin' => [
'label' => lng('serversettings.allow_error_report_admin'),
@@ -67,7 +70,8 @@ return [
'varname' => 'allow_error_report_admin',
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_allow_error_report_customer' => [
'label' => lng('serversettings.allow_error_report_customer'),
@@ -75,7 +79,8 @@ return [
'varname' => 'allow_error_report_customer',
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_allow_customer_shell' => [
'label' => lng('serversettings.allow_allow_customer_shell'),
@@ -84,7 +89,8 @@ return [
'type' => 'checkbox',
'default' => false,
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'system_available_shells' => [
'label' => lng('serversettings.available_shells'),
@@ -94,7 +100,8 @@ return [
'string_emptyallowed' => true,
'default' => '',
'save_method' => 'storeSettingField',
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
'system_froxlorusergroup' => [
'label' => lng('serversettings.froxlorusergroup'),
@@ -108,7 +115,8 @@ return [
'checkLocalGroup'
],
'visible' => Settings::Get('system.nssextrausers'),
'advanced_mode' => true
'advanced_mode' => true,
'required_otp' => true
],
]
]

View File

@@ -44,24 +44,30 @@ return [
'settinggroup' => 'system',
'varname' => 'diskquota_repquota_path',
'type' => 'text',
'string_type' => 'file',
'default' => '/usr/sbin/repquota',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_diskquota_quotatool_path' => [
'label' => lng('serversettings.diskquota_quotatool_path.description'),
'settinggroup' => 'system',
'varname' => 'diskquota_quotatool_path',
'type' => 'text',
'string_type' => 'file',
'default' => '/usr/bin/quotatool',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
],
'system_diskquota_customer_partition' => [
'label' => lng('serversettings.diskquota_customer_partition.description'),
'settinggroup' => 'system',
'varname' => 'diskquota_customer_partition',
'type' => 'text',
'string_type' => 'file',
'default' => '/dev/root',
'save_method' => 'storeSettingField'
'save_method' => 'storeSettingField',
'required_otp' => true
]
]
]

View File

@@ -77,7 +77,8 @@ if ($page == 'overview' && $userinfo['change_serversettings'] == '1') {
if (Form::processForm($settings_data, $_POST, [
'filename' => $filename,
'action' => $action,
'page' => $page
'page' => $page,
'part' => $_part,
], $_part, $settings_all, $settings_part, $only_enabledisable)) {
$log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "rebuild configfiles due to changed setting");
Cronjob::inserttask(TaskId::REBUILD_VHOST);

View File

@@ -25,10 +25,13 @@
namespace Froxlor;
use Exception;
use Froxlor\Api\Commands\Customers;
use Froxlor\Api\Commands\SubDomains;
use Froxlor\Database\Database;
use Froxlor\UI\Collection;
use Froxlor\UI\Response;
use RobThree\Auth\TwoFactorAuthException;
/**
* Class to manage the current user / session
@@ -169,4 +172,65 @@ class CurrentUser
return ($_SESSION['userinfo'][$resource . '_used'] < $_SESSION['userinfo'][$resource] || $_SESSION['userinfo'][$resource] == '-1') && $addition;
}
/**
* @throws TwoFactorAuthException
*/
public static function sendOtpEmail()
{
global $mail;
if (self::getField('type_2fa') == 1) {
// generate code
$tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$code = $tfa->getCode($tfa->createSecret());
// set code for user
$table = TABLE_PANEL_CUSTOMERS;
$uid = 'customerid';
if (self::isAdmin()) {
$table = TABLE_PANEL_ADMINS;
$uid = 'adminid';
}
$stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid");
Database::pexecute($stmt, [
"d2fa" => $code,
"uid" => self::getField($uid)
]);
// build up & send email
$_mailerror = false;
$mailerr_msg = "";
$replace_arr = [
'CODE' => $code
];
$mail_body = html_entity_decode(PhpHelper::replaceVariables(lng('mails.2fa.mailbody'), $replace_arr));
try {
$mail->Subject = lng('mails.2fa.subject');
$mail->AltBody = $mail_body;
$mail->MsgHTML(str_replace("\n", "<br />", $mail_body));
$mail->AddAddress(self::getField('email'), User::getCorrectUserSalutation(self::getData()));
$mail->Send();
} catch (\PHPMailer\PHPMailer\Exception $e) {
$mailerr_msg = $e->errorMessage();
$_mailerror = true;
} catch (Exception $e) {
$mailerr_msg = $e->getMessage();
$_mailerror = true;
}
if ($_mailerror) {
$rstlog = FroxlorLogger::getInstanceOf([
'loginname' => '2fa code-sending'
]);
$rstlog->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg);
Response::redirectTo('index.php', [
'showmessage' => '4',
'customermail' => self::getField('email')
]);
exit();
}
$mail->ClearAddresses();
}
}
}

View File

@@ -129,7 +129,8 @@ class Settings
{
// set defaults
self::$conf = [
'enable_webupdate' => false
'enable_webupdate' => false,
'disable_otp_security_check' => false,
];
$configfile = Froxlor::getInstallDir() . '/lib/config.inc.php';

View File

@@ -25,6 +25,8 @@
namespace Froxlor\UI;
use Froxlor\CurrentUser;
use Froxlor\FroxlorTwoFactorAuth;
use Froxlor\Settings;
use Froxlor\Validate\Check;
@@ -183,6 +185,21 @@ class Form
}
}
// OTP security validation for sensitive settings
if (!Settings::Config('disable_otp_security_check') && isset($fielddata['required_otp']) && $do_show) {
$otp_enabled_system = (bool)Settings::Get('2fa.enabled');
$otp_enabled_user = (int)CurrentUser::getField('type_2fa') != 0;
$do_show = !$fielddata['required_otp'] || ($otp_enabled_system && $otp_enabled_user);
if (!$do_show) {
$fielddata['note'] = lng('serversettings.option_required_otp');
if (!$otp_enabled_system) {
$fielddata['note'] .= '<br>' . lng('2fa.2fa_not_activated');
} elseif (!$otp_enabled_user) {
$fielddata['note'] .= '<br>' . lng('2fa.2fa_not_activated_for_user');
}
}
}
if (!$do_show) {
$fielddata['visible'] = false;
}
@@ -283,6 +300,38 @@ class Form
}
}
}
if (!Settings::Config('disable_otp_security_check') && isset($fielddetails['required_otp']) && isset($changed_fields[$fieldname])) {
$otp_enabled_system = (bool)Settings::Get('2fa.enabled');
$otp_enabled_user = (int)CurrentUser::getField('type_2fa') != 0;
$do_update = !$fielddetails['required_otp'] || ($otp_enabled_system && $otp_enabled_user);
if ($do_update) {
// setting that requires OTP verification
if (empty($input['otp_verification'])) {
// in case email 2fa is enabled, send it now
CurrentUser::sendOtpEmail();
// build up form
if (is_array($url_params) && isset($url_params['filename'])) {
$filename = $url_params['filename'];
unset($url_params['filename']);
} else {
$filename = '';
}
HTML::askOTP('please_enter_otp', $filename, array_merge($url_params, $submitted_fields));
} else {
// validate given OTP code
$code = trim($input['otp_verification']);
$tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$result = $tfa->verifyCode(CurrentUser::getField('data_2fa'), $code, 3);
if (!$result) {
Response::standardError('otpnotvalidated');
}
}
} else {
// do not update this setting
unset($changed_fields[$fieldname]);
}
}
}
}
}

View File

@@ -221,4 +221,17 @@ class HTML
]);
exit();
}
public static function askOTP(string $text, string $targetfile, array $params = [], string $replacer = '', array $back_link = [])
{
$text = lng('question.' . $text, [htmlspecialchars($replacer)]);
Panel\UI::view('form/otpquestion.html.twig', [
'action' => $targetfile,
'url_params' => $params,
'question' => $text,
'back_link' => $back_link
]);
exit();
}
}

View File

@@ -11,4 +11,9 @@ return [
* updates the way the providers does it (e.g. automation, etc.)
*/
'enable_webupdate' => false,
/**
* @todo description
*/
'disable_otp_security_check' => false,
];

View File

@@ -44,7 +44,8 @@ return [
'type' => 'text',
'maxlength' => 255,
'value' => 'service php7.4-fpm restart',
'mandatory' => true
'mandatory' => true,
'required_otp' => true
],
'config_dir' => [
'label' => lng('serversettings.phpfpm_settings.configdir'),

View File

@@ -45,7 +45,8 @@ return [
'type' => 'text',
'maxlength' => 255,
'value' => $result['reload_cmd'],
'mandatory' => true
'mandatory' => true,
'required_otp' => true
],
'config_dir' => [
'label' => lng('serversettings.phpfpm_settings.configdir'),

View File

@@ -46,7 +46,8 @@ return [
'label' => lng('admin.phpsettings.binary'),
'type' => 'text',
'maxlength' => 255,
'value' => '/usr/bin/php-cgi'
'value' => '/usr/bin/php-cgi',
'required_otp' => true
],
'fpmconfig' => [
'visible' => Settings::Get('phpfpm.enabled') == 1,
@@ -61,7 +62,8 @@ return [
'desc' => lng('admin.phpsettings.file_extensions_note'),
'type' => 'text',
'maxlength' => 255,
'value' => 'php'
'value' => 'php',
'required_otp' => true
],
'mod_fcgid_starter' => [
'visible' => Settings::Get('system.mod_fcgid') == 1,
@@ -181,7 +183,8 @@ return [
'cols' => 80,
'rows' => 20,
'value' => $result['phpsettings'],
'mandatory' => true
'mandatory' => true,
'required_otp' => true
],
'allow_all_customers' => [
'label' => lng('serversettings.phpfpm_settings.allow_all_customers.title'),

View File

@@ -47,7 +47,8 @@ return [
'label' => lng('admin.phpsettings.binary'),
'type' => 'text',
'maxlength' => 255,
'value' => $result['binary']
'value' => $result['binary'],
'required_otp' => true
],
'fpmconfig' => [
'visible' => Settings::Get('phpfpm.enabled') == 1,
@@ -62,7 +63,8 @@ return [
'desc' => lng('admin.phpsettings.file_extensions_note'),
'type' => 'text',
'maxlength' => 255,
'value' => $result['file_extensions']
'value' => $result['file_extensions'],
'required_otp' => true
],
'mod_fcgid_starter' => [
'visible' => Settings::Get('system.mod_fcgid') == 1,
@@ -185,7 +187,8 @@ return [
'cols' => 80,
'rows' => 20,
'value' => $result['phpsettings'],
'mandatory' => true
'mandatory' => true,
'required_otp' => true
],
'allow_all_customers' => [
'label' => lng('serversettings.phpfpm_settings.allow_all_customers.title'),

View File

@@ -0,0 +1,31 @@
{% extends "Froxlor/userarea.html.twig" %}
{% block content %}
<form action="{{ action|default("") }}" method="post" enctype="application/x-www-form-urlencoded" class="form">
<div class="alert alert-warning" role="alert">
<h4 class="alert-heading">{{ lng('panel.security_question') }}</h4>
<p>{{ question|raw }}</p>
<p>
<input name="otp_verification" id="otp_verification" type="text" class="form-control"
placeholder="{{ lng('login.2facode') }}" autocomplete="off" autofocus required/>
</p>
<p>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"/>
<input type="hidden" name="send" value="send"/>
{% for id,field in url_params %}
<input type="hidden" name="{{ id }}" value="{{ field }}"/>
{% endfor %}
<button class="btn btn-danger" type="submit" name="submitbutton">{{ lng('2fa.2fa_verify') }}</button>&nbsp;
{% if back_link is defined and back_link is iterable and back_link|length > 0 %}
<a href="{{ linker(back_link) }}" class="btn btn-secondary">{{ lng('panel.cancel') }}</a>
{% else %}
<a href="javascript:history.back(-1)" class="btn btn-secondary">{{ lng('panel.cancel') }}</a>
{% endif %}
</p>
</div>
</form>
{% endblock %}