diff -urN ../roundcubemail-1.6.1.orig/config/defaults.inc.php ./config/defaults.inc.php --- ../roundcubemail-1.6.1.orig/config/defaults.inc.php 2023-01-23 22:03:13.000000000 +0200 +++ ./config/defaults.inc.php 2023-03-21 14:55:30.612374000 +0200 @@ -576,8 +576,12 @@ $config['login_username_filter'] = null; // Brute-force attacks prevention. -// The value specifies maximum number of failed logon attempts per minute. +// The value specifies maximum number of failed logon attempts per $config['login_rate_limit_period']. $config['login_rate_limit'] = 3; +// The value specifies rate limit period in seconds. +$config['login_rate_limit_period'] = 60; +// The value specifies rate limit block_period in seconds. +$config['login_rate_limit_block_period'] = 300; // Includes should be interpreted as PHP files $config['skin_include_php'] = false; diff -urN ../roundcubemail-1.6.1.orig/program/lib/Roundcube/rcube_user.php ./program/lib/Roundcube/rcube_user.php --- ../roundcubemail-1.6.1.orig/program/lib/Roundcube/rcube_user.php 2023-01-23 22:03:14.000000000 +0200 +++ ./program/lib/Roundcube/rcube_user.php 2023-03-21 17:03:28.897928000 +0200 @@ -519,20 +519,44 @@ */ function failed_login() { - if ($this->ID && $this->rc->config->get('login_rate_limit', 3)) { + $login_rate_limit = $this->rc->config->get('login_rate_limit', 3); + if (!is_numeric($login_rate_limit)) $login_rate_limit = 3; + if ($this->ID && $login_rate_limit) { $counter = 0; + // don't log full session id for security reasons + $session_id = session_id(); + $session_id = $session_id ? substr($session_id, 0, 16) : 'no-session'; + if (empty($this->data['failed_login'])) { $failed_login = new DateTime('now'); $counter = 1; + + rcube::write_log('userlogins', sprintf("Failed login rate limit: set failed login counter to 1 for %s from %s in session %s", + $this->data['username'], rcube_utils::remote_ip(), $session_id)); } else { + $login_rate_limit_period = $this->rc->config->get('login_rate_limit_period', 60); + if (!is_numeric($login_rate_limit_period)) $login_rate_limit_period = 60; + $login_rate_limit_block_period = $this->rc->config->get('login_rate_limit_block_period', 300); + if (!is_numeric($login_rate_limit_block_period)) $login_rate_limit_block_period = 300; + $failed_login = new DateTime($this->data['failed_login']); - $threshold = new DateTime('- 60 seconds'); + $threshold = new DateTime('- ' . $login_rate_limit_period . ' seconds'); + $threshold_block = new DateTime('- ' . $login_rate_limit_block_period . ' seconds'); - if ($failed_login < $threshold) { + if ( + (($this->data['failed_login_counter'] < $login_rate_limit) && ($failed_login < $threshold)) or + (($this->data['failed_login_counter'] >= $login_rate_limit) && ($failed_login < $threshold_block)) + ) { $failed_login = new DateTime('now'); $counter = 1; + + rcube::write_log('userlogins', sprintf("Failed login rate limit: reset failed login counter to 1 for %s from %s in session %s", + $this->data['username'], rcube_utils::remote_ip(), $session_id)); + } else { + rcube::write_log('userlogins', sprintf("Failed login rate limit: increase failed login counter to %d for %s from %s in session %s", + $this->data['failed_login_counter'] + 1, $this->data['username'], rcube_utils::remote_ip(), $session_id)); } } @@ -556,10 +580,19 @@ } if ($rate = (int) $this->rc->config->get('login_rate_limit', 3)) { + $login_rate_limit_block_period = $this->rc->config->get('login_rate_limit_block_period', 300); + if (!is_numeric($login_rate_limit_block_period)) $login_rate_limit_block_period = 300; + $last_failed = new DateTime($this->data['failed_login']); - $threshold = new DateTime('- 60 seconds'); + $threshold = new DateTime('- ' . $login_rate_limit_block_period . ' seconds'); if ($last_failed > $threshold && $this->data['failed_login_counter'] >= $rate) { + // don't log full session id for security reasons + $session_id = session_id(); + $session_id = $session_id ? substr($session_id, 0, 16) : 'no-session'; + rcube::write_log('userlogins', sprintf("Failed login rate limit: account %s is locked in session %s", + $this->data['username'], $session_id)); + return true; } }