Skip to content

Commit

Permalink
Merge pull request #4496 from corentin-soriano/anti_bruteforce
Browse files Browse the repository at this point in the history
Anti bruteforce improvement
  • Loading branch information
nilsteampassnet authored Nov 26, 2024
2 parents 1262138 + 6c4b144 commit 7f72d3c
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 18 deletions.
2 changes: 1 addition & 1 deletion includes/config/include.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
*/

define('TP_VERSION', '3.1.2');
define("UPGRADE_MIN_DATE", "1732264740");
define("UPGRADE_MIN_DATE", "1732630844");
define('TP_VERSION_MINOR', '170');
define('TP_TOOL_NAME', 'Teampass');
define('TP_ONE_DAY_SECONDS', 86400);
Expand Down
4 changes: 4 additions & 0 deletions includes/language/english.php
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,10 @@
'syslog_port' => 'Syslog port (default 514)',
'error_bad_credentials' => 'Login credentials do not correspond!',
'bruteforce_wait' => 'Too many failed attempts, your account is blocked until: ',
'bruteforce_unlock_at' => 'Account unlocked at (anti bruteforce): ',
'bruteforce_reset_account' => 'Reset anti bruteforce of user',
'bruteforce_reset_mail_subject' => 'TEAMPASS - Your account is disabled',
'bruteforce_reset_mail_body' => 'Hello #name#,<br/><br/>Your teampass account has been locked due to a large number of authentication failures.<br/><br/>You can unblock it by clicking on this link <a href="#reset_url#" target="_blank">#reset_url#</a><br/><br/>Automatic unlock: #unlock_at#',
'settings_ldap_usergroup' => 'LDAP group to search',
'settings_ldap_usergroup_tip' => 'Enter the LDAP group in the directory where allowed user logins are stored. Example: cn=sysadmins,ou=groups,dc=example,dc=com',
'server_password_change_enable' => 'Enable changing password on distant server (using ssh connection)',
Expand Down
4 changes: 4 additions & 0 deletions includes/language/french.php
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,10 @@
'syslog_port' => 'Port Syslog',
'error_bad_credentials' => 'Informations de connexion erronées',
'bruteforce_wait' => 'Trop de tentatives échouées, votre compte est bloqué jusqu&apos;à : ',
'bruteforce_unlock_at' => 'Déblocage du compte (anti bruteforce) : ',
'bruteforce_reset_account' => 'Réinitialiser l&apos;anti bruteforce de l&apos;utilisateur',
'bruteforce_reset_mail_subject' => 'TEAMPASS - Votre compte est désactivé',
'bruteforce_reset_mail_body' => 'Bonjour #name#,<br/><br/>Votre compte teampass a été verouillé en raison d&apos;un grand nombre d&apos;échecs d&apos;authentification.<br/><br/>Vous pouvez le débloquer en cliquant sur ce lien <a href="#reset_url#" target="_blank">#reset_url#</a><br/><br/>Déblocage automatique : #unlock_at#',
'settings_ldap_usergroup' => 'Groupe LDAP dans lequel faire la recherche',
'settings_ldap_usergroup_tip' => 'Groupe LDAP dans lequel les utilisateurs doivent être membre pour pouvoir se connecter. Exemple : cn=sysadmins,ou=groups,dc=example,dc=com',
'server_password_change_enable' => 'Activer le changement automatique du mot de passe du compte du serveur (en utilisant une connexion SSH)',
Expand Down
1 change: 1 addition & 0 deletions install/install.queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,7 @@ function encryptFollowingDefuse($message, $ascii_key)
`value` VARCHAR(500) NOT NULL,
`date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`unlock_at` TIMESTAMP NULL DEFAULT NULL,
`unlock_code` VARCHAR(50) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) CHARSET=utf8;"
);
Expand Down
14 changes: 14 additions & 0 deletions install/upgrade_run_3.1.php
Original file line number Diff line number Diff line change
Expand Up @@ -626,10 +626,24 @@
`value` VARCHAR(500) NOT NULL,
`date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`unlock_at` TIMESTAMP NULL DEFAULT NULL,
`unlock_code` VARCHAR(50) NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) CHARSET=utf8;"
);

// Add unlock_code column
try {
$alter_table_query = "
ALTER TABLE `" . $pre . "auth_failures`
ADD COLUMN `unlock_code` VARCHAR(50) NULL DEFAULT NULL;";
mysqli_begin_transaction($db_link);
mysqli_query($db_link, $alter_table_query);
mysqli_commit($db_link);
} catch (Exception $e) {
// Rollback transaction if index already exists.
mysqli_rollback($db_link);
}

//---<END 3.1.2


Expand Down
37 changes: 33 additions & 4 deletions pages/users.js.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ className: 'details-control',
'<li class="dropdown-item pointer tp-action" data-id="' + $(data).data('id') + '" data-action="new-otp"><i class="fas fa-mask mr-2"></i><?php echo $lang->get('generate_new_otp'); ?></li>' :
''
) +
'<li class="dropdown-item pointer tp-action" data-id="' + $(data).data('id') + '" data-fullname="' + $(data).data('fullname') + '" data-action="reset-antibruteforce"><i class="fas fa-lock mr-2"></i><?php echo $lang->get('bruteforce_reset_account'); ?></li>' +
'<li class="dropdown-item pointer tp-action" data-id="' + $(data).data('id') + '" data-fullname="' + $(data).data('fullname') + '" data-action="logs"><i class="fas fa-newspaper mr-2"></i><?php echo $lang->get('see_logs'); ?></li>' +
'<li class="dropdown-item pointer tp-action" data-id="' + $(data).data('id') + '" data-action="qrcode"><i class="fas fa-qrcode mr-2"></i><?php echo $lang->get('user_ga_code'); ?></li>' +
'<li class="dropdown-item pointer tp-action" data-id="' + $(data).data('id') + '" data-fullname="' + $(data).data('fullname') + '"data-action="access-rights"><i class="fas fa-sitemap mr-2"></i><?php echo $lang->get('user_folders_rights'); ?></li>' +
Expand Down Expand Up @@ -1182,7 +1183,35 @@ function(data) {
}
);

// ---
} else if ($(this).data('action') === 'reset-antibruteforce') {
toastr.remove();
toastr.info('<?php echo $lang->get('in_progress'); ?> ... <i class="fas fa-circle-notch fa-spin fa-2x"></i>');

const data = {
'user_id': $(this).data('id'),
};

$.post(
"sources/users.queries.php", {
type: "reset_antibruteforce",
data: prepareExchangedData(JSON.stringify(data), 'encode', '<?php echo $session->get('key'); ?>'),
key: "<?php echo $session->get('key'); ?>"
},
function(data) {
// Inform user
toastr.remove();
toastr.success(
'<?php echo $lang->get('done'); ?>',
'', {
timeOut: 1000
}
);

// refresh table content
oTable.ajax.reload();
}
);

} else if ($(this).data('action') === 'new-enc-code') {
// HIde
$('.content-header, .content').addClass('hidden');
Expand Down Expand Up @@ -1268,7 +1297,7 @@ function(data) {
$('input[type="checkbox"].flat-blue').iCheck({
checkboxClass: 'icheckbox_flat-blue',
});
$(document).on('click', '#warningModalButtonAction', function() {
$(document).one('click', '#warningModalButtonAction', function() {

// Show spinner
toastr.remove();
Expand Down Expand Up @@ -1344,7 +1373,7 @@ function(data) {
$('input[type="checkbox"].flat-blue').iCheck({
checkboxClass: 'icheckbox_flat-blue',
});
$(document).on('click', '#warningModalButtonAction', function() {
$(document).one('click', '#warningModalButtonAction', function() {
if ($('#user-to-delete').prop('checked') === false) {
$('#warningModal').modal('hide');
return false;
Expand Down Expand Up @@ -2443,7 +2472,7 @@ function(data) {
'<?php echo $lang->get('perform'); ?>',
'<?php echo $lang->get('cancel'); ?>'
);
$(document).on('click', '#warningModalButtonAction', function(event) {
$(document).one('click', '#warningModalButtonAction', function(event) {
event.preventDefault();
event.stopPropagation();
if ($('#ldap-user-name').val() !== "" && $('#ldap-user-roles :selected').length > 0) {
Expand Down
81 changes: 81 additions & 0 deletions self-unlock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

/**
* Teampass - a collaborative passwords manager.
* ---
* This file is part of the TeamPass project.
*
* TeamPass is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* TeamPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Certain components of this file may be under different licenses. For
* details, see the `licenses` directory or individual file headers.
* ---
* @file 2fa.js.php
* @author Nils Laumaillé ([email protected])
* @copyright 2009-2024 Teampass.net
* @license GPL-3.0
* @see https://www.teampass.net
*/


use Symfony\Component\HttpFoundation\Request as SymfonyRequest;

// Load functions
require_once __DIR__. '/includes/config/include.php';
require_once __DIR__.'/sources/main.functions.php';

// init
loadClasses();

// Get username and OTP from GET parameters
$request = SymfonyRequest::createFromGlobals();
$username = $request->query->get('login', '');
$otp = $request->query->get('otp', '');

// Redirect user to teampass if username or otp is not provided
if (empty($username) || empty($otp)) {
header('Location: ./index.php');
exit;
}

// Check for existing lock
$result = DB::queryFirstField(
'SELECT 1
FROM ' . prefixTable('auth_failures') . '
WHERE unlock_at = (
SELECT MAX(unlock_at)
FROM ' . prefixTable('auth_failures') . '
WHERE unlock_at > %s
AND source = %s AND value = %s)
AND unlock_code = %s',
date('Y-m-d H:i:s', time()),
'login',
$username,
$otp
);

// Delete all logs for this user if provided OTP is correct
if ($result) {
DB::delete(
prefixTable('auth_failures'),
'source = %s AND value = %s',
'login',
$username
);
}

// Redirect user to teampass
header('Location: ./index.php');
exit;
44 changes: 41 additions & 3 deletions sources/identify.php
Original file line number Diff line number Diff line change
Expand Up @@ -2617,7 +2617,7 @@ function identifyDoAzureChecks(
* @param string $source - The source of the failed attempt (login or remote_ip).
* @param string $value - The value for this source (username or IP address).
* @param int $limit - The failure attempt limit after which the account/IP
* will be locked.
* will be locked.
*/
function handleFailedAttempts($source, $value, $limit) {
// Count failed attempts from this source
Expand All @@ -2633,19 +2633,57 @@ function handleFailedAttempts($source, $value, $limit) {
$count++;

// Calculate unlock time if number of attempts exceeds limit
$unlock_at = $count >= $limit
$unlock_at = $count >= $limit
? date('Y-m-d H:i:s', time() + (($count - $limit + 1) * 600))
: NULL;

// Unlock account one time code
$unlock_code = ($count >= $limit && $source === 'login')
? generateQuickPassword(30, false)
: NULL;

// Insert the new failure into the database
DB::insert(
prefixTable('auth_failures'),
[
'source' => $source,
'value' => $value,
'unlock_at' => $unlock_at,
'unlock_code' => $unlock_code,
]
);

if ($unlock_at !== null && $source === 'login') {
$configManager = new ConfigManager();
$SETTINGS = $configManager->getAllSettings();
$lang = new Language($SETTINGS['default_language']);

// Get user email
$userInfos = DB::QueryFirstRow(
'SELECT email, name
FROM '.prefixTable('users').'
WHERE login = %s',
$value
);

// No valid email address for user
if (!$userInfos || !filter_var($userInfos['email'], FILTER_VALIDATE_EMAIL))
return;

$unlock_url = $SETTINGS['cpassman_url'].'/self-unlock.php?login='.$value.'&otp='.$unlock_code;

sendMailToUser(
$userInfos['email'],
$lang->get('bruteforce_reset_mail_body'),
$lang->get('bruteforce_reset_mail_subject'),
[
'#name#' => $userInfos['name'],
'#reset_url#' => $unlock_url,
'#unlock_at#' => $unlock_at,
],
true
);
}
}

/**
Expand All @@ -2659,7 +2697,7 @@ function handleFailedAttempts($source, $value, $limit) {
*/
function addFailedAuthentication($username, $ip) {
$user_limit = 10;
$ip_limit = 20;
$ip_limit = 30;

// Remove old logs (more than 24 hours)
DB::delete(
Expand Down
5 changes: 3 additions & 2 deletions sources/main.functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4313,11 +4313,12 @@ function sendMailToUser(
global $SETTINGS;
$emailSettings = new EmailSettings($SETTINGS);
$emailService = new EmailService();
$antiXss = new AntiXSS();

// Sanitize inputs
$post_receipt = filter_var($post_receipt, FILTER_SANITIZE_EMAIL);
$post_subject = htmlspecialchars($post_subject, ENT_QUOTES, 'UTF-8');
$post_body = htmlspecialchars($post_body, ENT_QUOTES, 'UTF-8');
$post_subject = $antiXss->xss_clean($post_subject);
$post_body = $antiXss->xss_clean($post_body);

if (count($post_replace) > 0) {
$post_body = str_replace(
Expand Down
21 changes: 13 additions & 8 deletions sources/users.datatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,6 @@

// Display Grid
if ($showUserFolders === true) {
/*
// Build list of available users
if ((int) $record['admin'] !== 1 && (int) $record['disabled'] !== 1) {
$listAvailableUsers .= '<option value="'.$record['id'].'">'.$record['login'].'</option>';
}
*/

// Get list of allowed functions
$listAlloFcts = '';
if ((int) $record['admin'] !== 1) {
Expand All @@ -251,6 +244,16 @@
$record['id']
);

// Check for existing lock
$unlock_at = DB::queryFirstField(
'SELECT MAX(unlock_at)
FROM ' . prefixTable('auth_failures') . '
WHERE unlock_at > %s AND source = %s AND value = %s',
date('Y-m-d H:i:s', time()),
'login',
$record['login']
);

// Get some infos about user
$userDisplayInfos =
(isset($userDate['date']) ? '<i class=\"fas fa-calendar-day infotip text-info ml-2\" title=\"'.$lang->get('creation_date').': '.date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], (int) $userDate['date']).'\"></i>' : '')
Expand All @@ -265,7 +268,9 @@
((in_array($record['id'], [OTV_USER_ID, TP_USER_ID, SSH_USER_ID, API_USER_ID]) === false && (int) $record['admin'] !== 1 && ((int) $SETTINGS['duo'] === 1 || (int) $SETTINGS['google_authentication'] === 1)) ?
((int) $record['mfa_enabled'] === 1 ? '' : '<i class=\"fa-solid fa-fingerprint infotip ml-1\" style=\"color:Tomato\" title=\"'.$lang->get('mfa_disabled_for_user').'\"></i>') :
''
);
)
.
(($unlock_at) ? '<i class=\"fas fa-solid text-red fa-lock infotip text-info ml-1\" title=\"'.$lang->get('bruteforce_unlock_at').$unlock_at.'\"></i>' : '');
if ($request->query->filter('display_warnings', '', FILTER_VALIDATE_BOOLEAN) === true) {
$userDisplayInfos .= '<br>'.
((in_array($record['id'], [OTV_USER_ID, TP_USER_ID, SSH_USER_ID, API_USER_ID]) === false && (int) $record['admin'] !== 1 && is_null($record['keys_recovery_time']) === true) ?
Expand Down
26 changes: 26 additions & 0 deletions sources/users.queries.php
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,32 @@
'encode'
);

break;

case "reset_antibruteforce":
// Check KEY
if ($post_key !== $session->get('key')) {
echo prepareExchangedData(
array(
'error' => true,
'message' => $lang->get('key_is_not_correct'),
),
'encode'
);
break;
}

// Prepare variables
$login = getFullUserInfos((int) $dataReceived['user_id'])['login'];

// Delete all logs for this user
DB::delete(
prefixTable('auth_failures'),
'source = %s AND value = %s',
'login',
$login
);

break;
}
// # NEW LOGIN FOR USER HAS BEEN DEFINED ##
Expand Down

0 comments on commit 7f72d3c

Please sign in to comment.