Merge branch 'develop' into main
commit
bc733e6704
|
@ -9,6 +9,10 @@ class InitialSchema extends AbstractMigration
|
|||
{
|
||||
public function change()
|
||||
{
|
||||
$exists = $this->hasTable('broods');
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');
|
||||
$this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';");
|
||||
$this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';");
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
final class RegistrationFloodProtection 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('flood_protections');
|
||||
if (!$exists) {
|
||||
$table = $this->table('flood_protections', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$table
|
||||
->addColumn('remote_ip', 'string', [
|
||||
'null' => false,
|
||||
'length' => 45,
|
||||
])
|
||||
->addColumn('request_action', 'string', [
|
||||
'null' => false,
|
||||
'length' => 191,
|
||||
])
|
||||
->addColumn('expiration', 'integer', [
|
||||
'null' => false,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addIndex('remote_ip')
|
||||
->addIndex('request_action')
|
||||
->addIndex('expiration');
|
||||
$table->create();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ class AppController extends Controller
|
|||
public $restResponsePayload = null;
|
||||
public $user = null;
|
||||
public $breadcrumb = [];
|
||||
public $request_ip = null;
|
||||
|
||||
/**
|
||||
* Initialization hook method.
|
||||
|
@ -83,6 +84,7 @@ class AppController extends Controller
|
|||
Configure::write('DebugKit.forceEnable', true);
|
||||
}
|
||||
$this->loadComponent('CustomPagination');
|
||||
$this->loadComponent('FloodProtection');
|
||||
/*
|
||||
* Enable the following component for recommended CakePHP form protection settings.
|
||||
* see https://book.cakephp.org/4/en/controllers/components/form-protection.html
|
||||
|
@ -147,6 +149,9 @@ class AppController extends Controller
|
|||
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
}
|
||||
if (mt_rand(1, 50) === 1) {
|
||||
$this->FloodProtection->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private function authApiUser(): void
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Component;
|
||||
|
||||
use Cake\Controller\Component;
|
||||
use App\Model\Entity\User;
|
||||
use App\Http\Exception\TooManyRequestsException;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Core\Configure\Engine\PhpConfig;
|
||||
|
||||
class FloodProtectionComponent extends Component
|
||||
{
|
||||
private $remote_ip = null;
|
||||
private $FloodProtections = null;
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
$ip_source = Configure::check('security.logging.ip_source') ? Configure::read('security.logging.ip_source') : 'REMOTE_ADDR';
|
||||
$this->remote_ip = $_SERVER[$ip_source];
|
||||
$temp = explode(PHP_EOL, $_SERVER[$ip_source]);
|
||||
if (count($temp) > 1) {
|
||||
$this->remote_ip = $temp[0];
|
||||
}
|
||||
$this->FloodProtections = TableRegistry::getTableLocator()->get('FloodProtections');
|
||||
}
|
||||
|
||||
public function check(string $action, int $limit = 5, int $expiration_time = 300): bool
|
||||
{
|
||||
$results = $this->FloodProtections->find('all')->where(['request_action' => $action, 'remote_ip' => $this->remote_ip, 'expiration' > time()])->toList();
|
||||
if (count($results) >= $limit) {
|
||||
throw new TooManyRequestsException(__('Too many {0} requests have been issued ({1} requests allowed ever {2} seconds)', [$action, $limit, $expiration_time]));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function set(string $action, int $expiration_time = 300): bool
|
||||
{
|
||||
$entry = $this->FloodProtections->newEmptyEntity();
|
||||
$entry->expiration = time() + $expiration_time;
|
||||
$entry->remote_ip = $this->remote_ip;
|
||||
$entry->request_action = $action;
|
||||
return (bool)$this->FloodProtections->save($entry);
|
||||
|
||||
}
|
||||
|
||||
public function checkAndSet(string $action, int $limit = 5, int $expiration_time = 300): bool
|
||||
{
|
||||
$result = $this->check($action, $limit, $expiration_time);
|
||||
$this->set($action, $expiration_time);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->FloodProtections->deleteAll(['expiration <' => time()]);
|
||||
}
|
||||
}
|
|
@ -311,6 +311,9 @@ class UsersController extends AppController
|
|||
if (empty(Configure::read('security.registration.self-registration'))) {
|
||||
throw new UnauthorizedException(__('User self-registration is not open.'));
|
||||
}
|
||||
if (!empty(Configure::read('security.registration.floodProtection'))) {
|
||||
$this->FloodProtection->check('register');
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$data = $this->request->getData();
|
||||
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
|
@ -327,6 +330,9 @@ class UsersController extends AppController
|
|||
],
|
||||
];
|
||||
$processorResult = $processor->create($data);
|
||||
if (!empty(Configure::read('security.registration.floodProtection'))) {
|
||||
$this->FloodProtection->set('register');
|
||||
}
|
||||
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
|
||||
}
|
||||
$this->viewBuilder()->setLayout('login');
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace App\Http\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Represents an HTTP 404 error.
|
||||
*/
|
||||
class TooManyRequestsException extends \Cake\Http\Exception\HttpException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_defaultCode = 429;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|null $message If no message is given 'Too Many Requests' will be the message
|
||||
* @param int|null $code Status code, defaults to 429
|
||||
* @param \Throwable|null $previous The previous exception.
|
||||
*/
|
||||
public function __construct(?string $message = null, ?int $code = null, ?Throwable $previous = null)
|
||||
{
|
||||
if (empty($message)) {
|
||||
$message = 'Too Many Requests';
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
class FloodProtectionsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->setDisplayField('request_ip');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
return $validator;
|
||||
}
|
||||
}
|
|
@ -274,6 +274,21 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
]
|
||||
],
|
||||
'Security' => [
|
||||
'Logging' => [
|
||||
'Logging' => [
|
||||
'security.logging.ip_source' => [
|
||||
'name' => __('Set IP source'),
|
||||
'type' => 'select',
|
||||
'description' => __('Select where the harvested IP should come from. This defaults to REMOTE_ADDR, but for instances behind a proxy HTTP_X_FORWARDED_FOR or HTTP_CLIENT_IP might make more sense.'),
|
||||
'default' => 'REMOTE_ADDR',
|
||||
'options' => [
|
||||
'REMOTE_ADDR' => 'REMOTE_ADDR',
|
||||
'HTTP_X_FORWARDED_FOR' => 'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_CLIENT_IP' => __('HTTP_CLIENT_IP'),
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'Registration' => [
|
||||
'Registration' => [
|
||||
'security.registration.self-registration' => [
|
||||
|
@ -282,6 +297,12 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'),
|
||||
'default' => false,
|
||||
],
|
||||
'security.registration.floodProtection' => [
|
||||
'name' => __('Enable registration flood-protection'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('Enabling this setting will only allow 5 registrations / IP address every 15 minutes (rolling time-frame).'),
|
||||
'default' => false,
|
||||
],
|
||||
]
|
||||
],
|
||||
'Development' => [
|
||||
|
|
Loading…
Reference in New Issue