Merge branch 'develop' into main

pull/92/head
iglocska 2022-02-07 02:15:15 +01:00
commit bc733e6704
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
8 changed files with 205 additions and 0 deletions

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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' => [