new: [security] Bruteforce protection added

- logins allow for 5 attempts every 5 minutes
- Code ported and updated from MISP

- As reported by SK-CERT
cli-modification-summary
iglocska 2022-09-19 00:25:15 +02:00
parent 07a8d1dfcb
commit af1e2fd632
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
4 changed files with 163 additions and 23 deletions

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class Bruteforces extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change(): void
{
$exists = $this->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();
}
}
}

View File

@ -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');
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Bruteforce extends AppModel
{
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\ORM\TableRegistry;
class BruteforcesTable extends AppTable
{
private $logModel = null;
public function initialize(array $config): void
{
parent::initialize($config);
$this->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;
}
}
}