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

@@ -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'),