CVE-2023-2666 (published 2023-05-12T01:15Z)

Allocation of Resources Without Limits or Throttling in GitHub repository froxlor/froxlor prior to 2.0.16.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/froxlor/froxlor
commit_details: repo_url: https://github.com/froxlor/froxlor
author: Michael Kaufmann
author_date: 2023-05-02 10:19:53+02:00
msg: introduce http-request rate-limit; smaller fixes

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
repository: repo_url: https://github.com/froxlor/froxlor
repo_name: froxlor/Froxlor
description: The server administration software for your needs - The official Froxlor development Git repository
repo_language: PHP
owner: froxlor
file_changes: filename: 110.accounts.php
change_type: ModificationType.MODIFY
diff: @@ -138,6 +138,26 @@ return [
 					'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',

programming_language: PHP
filename: froxlor.sql.php
change_type: ModificationType.MODIFY
diff: @@ -699,6 +699,8 @@ opcache.validate_timestamps'),
 	('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'),
@@ -743,7 +745,7 @@ opcache.validate_timestamps'),
 	('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`;

programming_language: SQL
filename: install.php
change_type: ModificationType.MODIFY
diff: @@ -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;
 
@@ -62,6 +63,7 @@ require dirname(__DIR__) . '/lib/tables.inc.php';
 // init twig
 UI::initTwig(true);
 UI::sendHeaders();
+RateLimiter::run(true);
 
 $installer = new Install();
 $installer->handle();

programming_language: PHP
filename: update_2.x.inc.php
change_type: ModificationType.MODIFY
diff: @@ -463,3 +463,12 @@ if (Froxlor::isFroxlorVersion('2.0.14')) {
 	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');
+}

programming_language: PHP
filename: Api.php
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,7 @@
 namespace Froxlor\Api;
 
 use Exception;
+use Froxlor\Http\RateLimiter;
 use Froxlor\Settings;
 use voku\helper\AntiXSS;
 
@@ -52,6 +53,8 @@ class Api
 		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();
 	}
 
 	/**

programming_language: PHP
filename: CliCommand.php
change_type: ModificationType.MODIFY
diff: @@ -122,7 +122,7 @@ class CliCommand extends Command
 		include_once Froxlor::getInstallDir() . '/lib/tables.inc.php';
 		define('_CRON_UPDATE', 1);
 		ob_start([
-			'this',
+			$this,
 			'cleanUpdateOutput'
 		]);
 		include_once Froxlor::getInstallDir() . '/install/updatesql.php';

programming_language: PHP
filename: Froxlor.php
change_type: ModificationType.MODIFY
diff: @@ -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 = '';

programming_language: PHP
filename: RateLimiter.php
change_type: ModificationType.ADD
diff: @@ -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);
+	}
+}

programming_language: PHP
filename: PhpHelper.php
change_type: ModificationType.MODIFY
diff: @@ -519,7 +519,12 @@ class PhpHelper
 				} 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));

programming_language: PHP
filename: init.php
change_type: ModificationType.MODIFY
diff: @@ -52,6 +52,7 @@ require dirname(__DIR__) . '/vendor/autoload.php';
 use Froxlor\CurrentUser;
 use Froxlor\Froxlor;
 use Froxlor\FroxlorLogger;
+use Froxlor\Http\RateLimiter;
 use Froxlor\Idna\IdnaWrapper;
 use Froxlor\Language;
 use Froxlor\PhpHelper;
@@ -121,6 +122,7 @@ if (!isset($sql) || !is_array($sql)) {
 
 // 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();

programming_language: PHP
filename: de.lng.php
change_type: ModificationType.MODIFY
diff: @@ -2072,6 +2072,14 @@ Vielen Dank, Ihr Administrator',
 			'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?',

programming_language: PHP
filename: en.lng.php
change_type: ModificationType.MODIFY
diff: @@ -2196,6 +2196,14 @@ Yours sincerely, your administrator',
 			'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?',

programming_language: PHP