new: [security] Bruteforce protection added
- logins allow for 5 attempts every 5 minutes - Code ported and updated from MISP - As reported by SK-CERTcli-modification-summary
parent
07a8d1dfcb
commit
af1e2fd632
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -297,6 +297,18 @@ class UsersController extends AppController
|
|||
|
||||
public function login()
|
||||
{
|
||||
$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 (!$blocked) {
|
||||
$result = $this->Authentication->getResult();
|
||||
// If the user is logged in send them away.
|
||||
$logModel = $this->Users->auditLogs();
|
||||
|
@ -313,6 +325,7 @@ class UsersController extends AppController
|
|||
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',
|
||||
|
@ -322,6 +335,7 @@ class UsersController extends AppController
|
|||
]);
|
||||
$this->Flash->error(__('Invalid username or password'));
|
||||
}
|
||||
}
|
||||
$this->viewBuilder()->setLayout('login');
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
use Cake\ORM\Entity;
|
||||
|
||||
class Bruteforce extends AppModel
|
||||
{
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue