Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anti bruteforce improvement #4496

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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