From af1e2fd6325fcff883190cacb6ad76592a10e38f Mon Sep 17 00:00:00 2001 From: iglocska Date: Mon, 19 Sep 2022 00:25:15 +0200 Subject: [PATCH] new: [security] Bruteforce protection added - logins allow for 5 attempts every 5 minutes - Code ported and updated from MISP - As reported by SK-CERT --- .../Migrations/20220918000000_bruteforces.php | 46 +++++++++++++ src/Controller/UsersController.php | 60 +++++++++------- src/Model/Entity/Bruteforce.php | 11 +++ src/Model/Table/BruteforcesTable.php | 69 +++++++++++++++++++ 4 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 config/Migrations/20220918000000_bruteforces.php create mode 100644 src/Model/Entity/Bruteforce.php create mode 100644 src/Model/Table/BruteforcesTable.php diff --git a/config/Migrations/20220918000000_bruteforces.php b/config/Migrations/20220918000000_bruteforces.php new file mode 100644 index 0000000..aefdad5 --- /dev/null +++ b/config/Migrations/20220918000000_bruteforces.php @@ -0,0 +1,46 @@ +hasTable('bruteforces'); + if (!$exists) { + $table = $this->table('bruteforces', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + $table + ->addColumn('user_ip', 'string', [ + 'null' => false, + 'length' => 45, + ]) + ->addColumn('username', 'string', [ + 'null' => false, + 'length' => 191, + 'collation' => 'utf8mb4_unicode_ci' + ]) + ->addColumn('expiration', 'datetime', [ + 'null' => false + ]) + ->addIndex('user_ip') + ->addIndex('username') + ->addIndex('expiration'); + $table->create(); + } + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 8405b3f..b0d35d2 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -297,30 +297,44 @@ class UsersController extends AppController public function login() { - $result = $this->Authentication->getResult(); - // If the user is logged in send them away. - $logModel = $this->Users->auditLogs(); - if ($result->isValid()) { - $user = $logModel->userInfo(); - $logModel->insert([ - 'request_action' => 'login', - 'model' => 'Users', - 'model_id' => $user['id'], - 'model_title' => $user['name'], - 'changed' => [] - ]); - $target = $this->Authentication->getLoginRedirect() ?? '/instance/home'; - return $this->redirect($target); + $blocked = false; + if ($this->request->is('post')) { + $BruteforceTable = TableRegistry::getTableLocator()->get('Bruteforces'); + $input = $this->request->getData(); + $blocked = $BruteforceTable->isBlocklisted($_SERVER['REMOTE_ADDR'], $input['username']); + if ($blocked) { + $this->Authentication->logout(); + $this->Flash->error(__('Too many attempts, brute force protection triggered. Wait 5 minutes before trying again.')); + $this->redirect(['controller' => 'users', 'action' => 'login']); + } } - if ($this->request->is('post') && !$result->isValid()) { - $logModel->insert([ - 'request_action' => 'login_fail', - 'model' => 'Users', - 'model_id' => 0, - 'model_title' => 'unknown_user', - 'changed' => [] - ]); - $this->Flash->error(__('Invalid username or password')); + if (!$blocked) { + $result = $this->Authentication->getResult(); + // If the user is logged in send them away. + $logModel = $this->Users->auditLogs(); + if ($result->isValid()) { + $user = $logModel->userInfo(); + $logModel->insert([ + 'request_action' => 'login', + 'model' => 'Users', + 'model_id' => $user['id'], + 'model_title' => $user['name'], + 'changed' => [] + ]); + $target = $this->Authentication->getLoginRedirect() ?? '/instance/home'; + return $this->redirect($target); + } + if ($this->request->is('post') && !$result->isValid()) { + $BruteforceTable->insert($_SERVER['REMOTE_ADDR'], $input['username']); + $logModel->insert([ + 'request_action' => 'login_fail', + 'model' => 'Users', + 'model_id' => 0, + 'model_title' => 'unknown_user', + 'changed' => [] + ]); + $this->Flash->error(__('Invalid username or password')); + } } $this->viewBuilder()->setLayout('login'); } diff --git a/src/Model/Entity/Bruteforce.php b/src/Model/Entity/Bruteforce.php new file mode 100644 index 0000000..ed0001e --- /dev/null +++ b/src/Model/Entity/Bruteforce.php @@ -0,0 +1,11 @@ +setDisplayField('email'); + $this->logModel = TableRegistry::getTableLocator()->get('AuditLogs'); + } + + public function insert($ip, $username) + { + $expire = 300; + $amount = 5; + $expire = time() + $expire; + $expire = date('Y-m-d H:i:s', $expire); + $bruteforceEntry = $this->newEntity([ + 'user_ip' => $ip, + 'username' => trim(strtolower($username)), + 'expiration' => $expire + ]); + $this->save($bruteforceEntry); + $title = 'Failed login attempt using username ' . $username . ' from IP: ' . $ip . '.'; + if ($this->isBlocklisted($ip, $username)) { + $title .= 'This has tripped the bruteforce protection after ' . $amount . ' failed attempts. The user is now blocklisted for ' . $expire . ' seconds.'; + } + $this->logModel->insert([ + 'request_action' => 'login_fail', + 'model' => 'Users', + 'model_id' => 0, + 'model_title' => 'bruteforce_block', + 'changed' => [] + ]); + } + + public function clean() + { + $expire = date('Y-m-d H:i:s', time()); + $this->deleteAll(['expiration <=' => $expire]); + } + + public function isBlocklisted($ip, $username) + { + // first remove old expired rows + $this->clean(); + // count + $count = $this->find('all', [ + 'conditions' => [ + 'user_ip' => $ip, + 'username' => trim($username) + ] + ])->count(); + if ($count >= 5) { + return true; + } else { + return false; + } + } +}