Added brute-force attack prevention via login rate limit (#1490566)

pull/309/head
Aleksander Machniak 9 years ago
parent 7caa9f5f50
commit a15d877ba8

@ -10,6 +10,7 @@ CHANGELOG Roundcube Webmail
- PGP encryption support via Mailvelope integration
- PGP encryption support via Enigma plugin
- PHP7 compatibility fixes (#1490416)
- Security: Added brute-force attack prevention via login rate limit (#1490566)
- Security: Added options to validate username/password on logon (#1490500)
- Security: Improve randomness of security tokens (#1490529)
- Security: Use random security tokens instead of hashes based on encryption key (#1490404)

@ -103,6 +103,8 @@ CREATE TABLE [dbo].[users] (
[mail_host] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL ,
[created] [datetime] NOT NULL ,
[last_login] [datetime] NULL ,
[failed_login] [datetime] NULL ,
[failed_login_counter] [int] NULL ,
[language] [varchar] (5) COLLATE Latin1_General_CI_AI NULL ,
[preferences] [text] COLLATE Latin1_General_CI_AI NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
@ -393,6 +395,6 @@ CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts]
WHERE [contact_id] IN (SELECT [contact_id] FROM deleted)
GO
INSERT INTO [dbo].[system] ([name], [value]) VALUES ('roundcube-version', '2015030800')
INSERT INTO [dbo].[system] ([name], [value]) VALUES ('roundcube-version', '2015111100')
GO

@ -0,0 +1,4 @@
ALTER TABLE [dbo].[users] ADD [failed_login] [datetime] NULL
GO
ALTER TABLE [dbo].[users] ADD [failed_login_counter] [int] NULL
GO

@ -24,6 +24,8 @@ CREATE TABLE `users` (
`mail_host` varchar(128) NOT NULL,
`created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`last_login` datetime DEFAULT NULL,
`failed_login` datetime DEFAULT NULL,
`failed_login_counter` int(10) UNSIGNED DEFAULT NULL,
`language` varchar(5),
`preferences` longtext,
PRIMARY KEY(`user_id`),
@ -209,4 +211,4 @@ CREATE TABLE `system` (
/*!40014 SET FOREIGN_KEY_CHECKS=1 */;
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100');

@ -0,0 +1,3 @@
ALTER TABLE `users`
ADD `failed_login` datetime DEFAULT NULL,
ADD `failed_login_counter` int(10) UNSIGNED DEFAULT NULL;

@ -7,6 +7,8 @@ CREATE TABLE "users" (
"mail_host" varchar(128) NOT NULL,
"created" timestamp with time zone DEFAULT current_timestamp NOT NULL,
"last_login" timestamp with time zone DEFAULT NULL,
"failed_login" timestamp with time zone DEFAULT NULL,
"failed_login_counter" integer DEFAULT NULL,
"language" varchar(5),
"preferences" long DEFAULT NULL,
CONSTRAINT "users_username_key" UNIQUE ("username", "mail_host")
@ -218,4 +220,4 @@ CREATE TABLE "system" (
"value" long
);
INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2015030800');
INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2015111100');

@ -0,0 +1,2 @@
ALTER TABLE "users" ADD "failed_login" timestamp with time zone DEFAULT NULL;
ALTER TABLE "users" ADD "failed_login_counter" integer DEFAULT NULL;

@ -22,6 +22,8 @@ CREATE TABLE users (
mail_host varchar(128) DEFAULT '' NOT NULL,
created timestamp with time zone DEFAULT now() NOT NULL,
last_login timestamp with time zone DEFAULT NULL,
failed_login timestamp with time zone DEFAULT NULL,
failed_login_counter integer DEFAULT NULL,
"language" varchar(5),
preferences text DEFAULT ''::text NOT NULL,
CONSTRAINT users_username_key UNIQUE (username, mail_host)
@ -290,4 +292,4 @@ CREATE TABLE "system" (
value text
);
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100');

@ -0,0 +1,2 @@
ALTER TABLE "users" ADD failed_login timestamp with time zone DEFAULT NULL;
ALTER TABLE "users" ADD failed_login_counter integer DEFAULT NULL;

@ -72,6 +72,8 @@ CREATE TABLE users (
mail_host varchar(128) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
last_login datetime DEFAULT NULL,
failed_login datetime DEFAULT NULL,
failed_login_counter integer DEFAULT NULL,
language varchar(5),
preferences text NOT NULL default ''
);
@ -201,4 +203,4 @@ CREATE TABLE system (
value text NOT NULL
);
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800');
INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100');

@ -0,0 +1,35 @@
CREATE TABLE tmp_users (
user_id integer NOT NULL PRIMARY KEY,
username varchar(128) NOT NULL default '',
mail_host varchar(128) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
last_login datetime DEFAULT NULL,
failed_login datetime DEFAULT NULL,
failed_login_counter integer DEFAULT NULL,
language varchar(5),
preferences text NOT NULL default ''
);
INSERT INTO tmp_users (user_id, username, mail_host, created, last_login, language, preferences)
SELECT user_id, username, mail_host, created, last_login, language, preferences FROM users;
DROP TABLE users;
CREATE TABLE users (
user_id integer NOT NULL PRIMARY KEY,
username varchar(128) NOT NULL default '',
mail_host varchar(128) NOT NULL default '',
created datetime NOT NULL default '0000-00-00 00:00:00',
last_login datetime DEFAULT NULL,
failed_login datetime DEFAULT NULL,
failed_login_counter integer DEFAULT NULL,
language varchar(5),
preferences text NOT NULL default ''
);
INSERT INTO users (user_id, username, mail_host, created, last_login, language, preferences)
SELECT user_id, username, mail_host, created, last_login, language, preferences FROM tmp_users;
CREATE UNIQUE INDEX ix_users_username ON users(username, mail_host);
DROP TABLE tmp_users;

@ -386,6 +386,10 @@ $config['login_password_maxlen'] = 1024;
// Example: '/^[a-z0-9_@.-]+$/'
$config['login_username_filter'] = null;
// Brute-force attacks prevention.
// The value specifies maximum number of failed logon attempts per minute.
$config['login_rate_limit'] = 3;
// Includes should be interpreted as PHP files
$config['skin_include_php'] = false;

@ -155,6 +155,7 @@ if ($RCMAIL->task == 'login' && $RCMAIL->action == 'login') {
RCMAIL::ERROR_COOKIES_DISABLED => 'cookiesdisabled',
RCMAIL::ERROR_INVALID_REQUEST => 'invalidrequest',
RCMAIL::ERROR_INVALID_HOST => 'invalidhost',
RCMAIL::ERROR_RATE_LIMIT => 'accountlocked',
);
$error_message = !empty($auth['error']) && !is_numeric($auth['error']) ? $auth['error'] : ($error_labels[$error_code] ?: 'loginfailed');

@ -60,6 +60,7 @@ class rcmail extends rcube
const ERROR_INVALID_REQUEST = 1;
const ERROR_INVALID_HOST = 2;
const ERROR_COOKIES_DISABLED = 3;
const ERROR_RATE_LIMIT = 4;
/**
@ -602,12 +603,22 @@ class rcmail extends rcube
// user already registered -> overwrite username
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
// Brute-force prevention
if ($user->is_locked()) {
$this->login_error = self::ERROR_RATE_LIMIT;
return false;
}
}
$storage = $this->get_storage();
// try to log in
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
if ($user) {
$user->failed_login();
}
// Wait a second to slow down brute-force attacks (#1490549)
sleep(1);
return false;

@ -481,12 +481,62 @@ class rcube_user
}
}
/**
* Update user's failed_login timestamp and counter
*/
function failed_login()
{
if ($this->ID && ($rate = (int) $this->rc->config->get('login_rate_limit', 3))) {
if (empty($this->data['failed_login'])) {
$failed_login = new DateTime('now');
$counter = 1;
}
else {
$failed_login = new DateTime($this->data['failed_login']);
$threshold = new DateTime('- 60 seconds');
if ($failed_login < $threshold) {
$failed_login = new DateTime('now');
$counter = 1;
}
}
$this->db->query(
"UPDATE " . $this->db->table_name('users', true)
. " SET `failed_login` = " . $this->db->fromunixtime($failed_login->format('U'))
. ", `failed_login_counter` = " . ($counter ?: "`failed_login_counter` + 1")
. " WHERE `user_id` = ?",
$this->ID);
}
}
/**
* Checks if the account is locked, e.g. as a result of brute-force prevention
*/
function is_locked()
{
if (empty($this->data['failed_login'])) {
return false;
}
if ($rate = (int) $this->rc->config->get('login_rate_limit', 3)) {
$last_failed = new DateTime($this->data['failed_login']);
$threshold = new DateTime('- 60 seconds');
if ($last_failed > $threshold && $this->data['failed_login_counter'] >= $rate) {
return true;
}
}
return false;
}
/**
* Clear the saved object state
*/
function reset()
{
$this->ID = null;
$this->ID = null;
$this->data = null;
}
@ -519,10 +569,11 @@ class rcube_user
}
// user already registered -> overwrite username
if ($sql_arr)
if ($sql_arr) {
return new rcube_user($sql_arr['user_id'], $sql_arr);
else
return false;
}
return false;
}
/**

@ -24,6 +24,7 @@ $messages['sessionerror'] = 'Your session is invalid or expired.';
$messages['storageerror'] = 'Connection to storage server failed.';
$messages['servererror'] = 'Server Error!';
$messages['servererrormsg'] = 'Server Error: $msg';
$messages['accountlocked'] = 'Too many failed login attempts. Try again later.';
$messages['connerror'] = 'Connection Error (Failed to reach the server)!';
$messages['dberror'] = 'Database Error!';
$messages['windowopenerror'] = 'The popup window was blocked!';

Loading…
Cancel
Save