Added brute-force attack prevention via login rate limit (#1490566)
5 files added
11 files modified
| | |
| | | - 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) |
| | |
| | | [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]
|
| | |
| | | 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
|
| | |
|
New file |
| | |
| | | ALTER TABLE [dbo].[users] ADD [failed_login] [datetime] NULL
|
| | | GO
|
| | | ALTER TABLE [dbo].[users] ADD [failed_login_counter] [int] NULL
|
| | | GO
|
| | |
| | | `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`), |
| | |
| | | |
| | | /*!40014 SET FOREIGN_KEY_CHECKS=1 */; |
| | | |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800'); |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100'); |
New file |
| | |
| | | ALTER TABLE `users` |
| | | ADD `failed_login` datetime DEFAULT NULL, |
| | | ADD `failed_login_counter` int(10) UNSIGNED DEFAULT NULL; |
| | |
| | | "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") |
| | |
| | | "value" long |
| | | ); |
| | | |
| | | INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2015030800'); |
| | | INSERT INTO "system" ("name", "value") VALUES ('roundcube-version', '2015111100'); |
New file |
| | |
| | | ALTER TABLE "users" ADD "failed_login" timestamp with time zone DEFAULT NULL; |
| | | ALTER TABLE "users" ADD "failed_login_counter" integer DEFAULT NULL; |
| | |
| | | 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) |
| | |
| | | value text |
| | | ); |
| | | |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800'); |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100'); |
New file |
| | |
| | | ALTER TABLE "users" ADD failed_login timestamp with time zone DEFAULT NULL; |
| | | ALTER TABLE "users" ADD failed_login_counter integer DEFAULT NULL; |
| | |
| | | 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 '' |
| | | ); |
| | |
| | | value text NOT NULL |
| | | ); |
| | | |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015030800'); |
| | | INSERT INTO system (name, value) VALUES ('roundcube-version', '2015111100'); |
New file |
| | |
| | | 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; |
| | |
| | | // 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; |
| | | |
| | |
| | | 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'); |
| | |
| | | const ERROR_INVALID_REQUEST = 1; |
| | | const ERROR_INVALID_HOST = 2; |
| | | const ERROR_COOKIES_DISABLED = 3; |
| | | const ERROR_RATE_LIMIT = 4; |
| | | |
| | | |
| | | /** |
| | |
| | | // 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; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | // 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | $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!'; |