Skip to content

Commit

Permalink
introduce http-request rate-limit; smaller fixes
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
  • Loading branch information
d00p committed May 2, 2023
1 parent 640466f commit 1679675
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 4 deletions.
20 changes: 20 additions & 0 deletions actions/admin/settings/110.accounts.php
Expand Up @@ -138,6 +138,26 @@
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'system_req_limit_per_interval' => [
'label' => lng('serversettings.req_limit_per_interval'),
'settinggroup' => 'system',
'varname' => 'req_limit_per_interval',
'type' => 'number',
'min' => 30,
'default' => 60,
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'system_req_limit_interval' => [
'label' => lng('serversettings.req_limit_interval'),
'settinggroup' => 'system',
'varname' => 'req_limit_interval',
'type' => 'number',
'min' => 5,
'default' => 60,
'save_method' => 'storeSettingField',
'advanced_mode' => true
],
'customer_accountprefix' => [
'label' => lng('serversettings.accountprefix'),
'settinggroup' => 'customer',
Expand Down
4 changes: 3 additions & 1 deletion install/froxlor.sql.php
Expand Up @@ -699,6 +699,8 @@
('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.15'),
('system', 'traffictool', 'goaccess'),
('system', 'req_limit_per_interval', 60),
('system', 'req_limit_interval', 60),
('api', 'enabled', '0'),
('api', 'customer_default', '1'),
('2fa', 'enabled', '1'),
Expand Down Expand Up @@ -743,7 +745,7 @@
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.15'),
('panel', 'db_version', '202303150');
('panel', 'db_version', '202304260');
DROP TABLE IF EXISTS `panel_tasks`;
Expand Down
2 changes: 2 additions & 0 deletions install/install.php
Expand Up @@ -23,6 +23,7 @@
* @license https://files.froxlor.org/misc/COPYING.txt GPLv2
*/

use Froxlor\Http\RateLimiter;
use Froxlor\UI\Panel\UI;
use Froxlor\Install\Install;

Expand Down Expand Up @@ -62,6 +63,7 @@
// init twig
UI::initTwig(true);
UI::sendHeaders();
RateLimiter::run(true);

$installer = new Install();
$installer->handle();
9 changes: 9 additions & 0 deletions install/updates/froxlor/update_2.x.inc.php
Expand Up @@ -463,3 +463,12 @@
Update::showUpdateStep("Updating from 2.0.14 to 2.0.15", false);
Froxlor::updateToVersion('2.0.15');
}

if (Froxlor::isDatabaseVersion('202303150')) {
Update::showUpdateStep("Adding new request rate limit settings");
Settings::AddNew("system.req_limit_per_interval", "60");
Settings::AddNew("system.req_limit_interval", "60");
Update::lastStepStatus(0);

Froxlor::updateToDbVersion('202304260');
}
3 changes: 3 additions & 0 deletions lib/Froxlor/Api/Api.php
Expand Up @@ -26,6 +26,7 @@
namespace Froxlor\Api;

use Exception;
use Froxlor\Http\RateLimiter;
use Froxlor\Settings;
use voku\helper\AntiXSS;

Expand All @@ -52,6 +53,8 @@ public function __construct()
if (Settings::Get('api.enabled') != 1) {
throw new Exception('API is not enabled. Please contact the administrator if you think this is wrong.', 400);
}

RateLimiter::run();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/Froxlor/Cli/CliCommand.php
Expand Up @@ -122,7 +122,7 @@ private function runUpdate(OutputInterface $output): int
include_once Froxlor::getInstallDir() . '/lib/tables.inc.php';
define('_CRON_UPDATE', 1);
ob_start([
'this',
$this,
'cleanUpdateOutput'
]);
include_once Froxlor::getInstallDir() . '/install/updatesql.php';
Expand Down
2 changes: 1 addition & 1 deletion lib/Froxlor/Froxlor.php
Expand Up @@ -34,7 +34,7 @@ final class Froxlor
const VERSION = '2.0.15';

// Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202303150';
const DBVERSION = '202304260';

// Distribution branding-tag (used for Debian etc.)
const BRANDING = '';
Expand Down
46 changes: 46 additions & 0 deletions lib/Froxlor/Http/RateLimiter.php
@@ -0,0 +1,46 @@
<?php

namespace Froxlor\Http;

class RateLimiter
{
private static int $limit_per_interval = 60;
private static int $reset_time = 0;

public static function run(bool $install_mode = false)
{
// default interval = 60 sec
self::$reset_time = time() + 60;

if (!$install_mode) {
self::$limit_per_interval = Settings::Get('system.req_limit_per_interval');
self::$reset_time = time() + Settings::Get('system.req_limit_interval');
}

// Get the remaining requests and reset time from the headers
$remaining = isset($_SESSION['HTTP_X_RATELIMIT_REMAINING']) ? (int)$_SESSION['HTTP_X_RATELIMIT_REMAINING'] : self::$limit_per_interval;
$reset = isset($_SESSION['HTTP_X_RATELIMIT_RESET']) ? (int)$_SESSION['HTTP_X_RATELIMIT_RESET'] : self::$reset_time;

// check if reset time is due
if (time() > $reset) {
$remaining = self::$limit_per_interval;
$reset = self::$reset_time;
}

// If we've hit the limit, return an error
if ($remaining <= 0) {
header('HTTP/1.1 429 Too Many Requests');
header("Retry-After: $reset");
exit();
}

// Decrement the remaining requests and update the headers
$remaining--;
$_SESSION['HTTP_X_RATELIMIT_REMAINING'] = $remaining;
$_SESSION['HTTP_X_RATELIMIT_RESET'] = $reset;

header("X-RateLimit-Limit: " . self::$limit_per_interval);
header("X-RateLimit-Remaining: " . $remaining);
header("X-RateLimit-Reset: " . $reset);
}
}
7 changes: 6 additions & 1 deletion lib/Froxlor/PhpHelper.php
Expand Up @@ -519,7 +519,12 @@ public static function parseArrayToString(array $array, string $key = null, int
} elseif (is_int($value)) {
$str .= self::tabPrefix($depth, "'{$key}' => $value,\n");
} else {
$str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n");
if ($key == 'password') {
// special case for passwords (nowdoc)
$str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n");
} else {
$str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n");
}
}
} else {
$str .= self::parseArrayToString($value, $key, ($depth + 1));
Expand Down
2 changes: 2 additions & 0 deletions lib/init.php
Expand Up @@ -52,6 +52,7 @@
use Froxlor\CurrentUser;
use Froxlor\Froxlor;
use Froxlor\FroxlorLogger;
use Froxlor\Http\RateLimiter;
use Froxlor\Idna\IdnaWrapper;
use Froxlor\Language;
use Froxlor\PhpHelper;
Expand Down Expand Up @@ -121,6 +122,7 @@

// send ssl-related headers (later than the others because we need a working database-connection and installation)
UI::sendSslHeaders();
RateLimiter::run();

// create a new idna converter
$idna_convert = new IdnaWrapper();
Expand Down
8 changes: 8 additions & 0 deletions lng/de.lng.php
Expand Up @@ -2072,6 +2072,14 @@
'toolselect' => 'Traffic Analyzer',
],
'requires_reconfiguration' => 'Änderungen an dieser Einstellungen benötigen unter Umständen eine erneute Konfiguration der folgenden Dienste:<br><strong>%s</strong>',
'req_limit_per_interval' => [
'title' => 'Anzahl der HTTP-Anfragen pro Intervall',
'description' => 'Erlaubte Anzahl von HTTP-Anfragen pro Intervall (siehe unten) auf froxlor, Standard ist "60"',
],
'req_limit_interval' => [
'title' => 'Rate-Limit-Intervall',
'description' => 'Specify the time in seconds for the number of HTTP requests, default is "60"',
],
],
'spf' => [
'use_spf' => 'Aktiviere SPF für Domains?',
Expand Down
8 changes: 8 additions & 0 deletions lng/en.lng.php
Expand Up @@ -2196,6 +2196,14 @@
'goaccess' => 'goacccess'
],
'requires_reconfiguration' => 'Changing this settings might require a reconfiguration of the following services:<br><strong>%s</strong>',
'req_limit_per_interval' => [
'title' => 'Number of HTTP requests per interval',
'description' => 'Limit the number of HTTP requests per interval (see below) to froxlor, default is "60"',
],
'req_limit_interval' => [
'title' => 'Rate-limit interval',
'description' => 'Zeit in Sekunden für die maximale Anzahl von HTTP-Anfragen, Standard ist "60".',
],
],
'spf' => [
'use_spf' => 'Activate SPF for domains?',
Expand Down

1 comment on commit 1679675

@abergmann
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CVE-2023-2666 was assigned to this commit.

Please sign in to comment.