Skip to content

Commit

Permalink
Add ability to rate limit via Validate class (#2998)
Browse files Browse the repository at this point in the history
  • Loading branch information
samerton committed Aug 7, 2022
1 parent cdd7d85 commit 98fe4b7
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 6 deletions.
61 changes: 59 additions & 2 deletions core/classes/Core/Validate.php
Expand Up @@ -81,6 +81,11 @@ class Validate {
*/
public const NOT_START_WITH = 'not_start_with';

/**
* @var string Set a rate limit
*/
public const RATE_LIMIT = 'rate_limit';

private DB $_db;

private ?string $_message = null;
Expand Down Expand Up @@ -112,6 +117,7 @@ private function __construct() {
* @param array $items subset of inputs to be validated
*
* @return Validate New instance of Validate.
* @throws Exception If provided configuration for a rule is invalid - not if a provided value is invalid!
*/
public static function check(array $source, array $items = []): Validate {
$validator = new Validate();
Expand Down Expand Up @@ -318,6 +324,51 @@ public static function check(array $source, array $items = []): Validate {
break;
}
break;

case self::RATE_LIMIT:
if (is_array($rule_value) && count($rule_value) === 2) {
// If array treat as [limit, seconds]
[$limit, $seconds] = $rule_value;
} else if (is_int($rule_value)) {
// If integer default seconds to 60
[$limit, $seconds] = [$rule_value, 60];
}

if (!isset($limit) || !isset($seconds)) {
throw new Exception('Invalid rate limit configuration');
}

$key = "rate_limit_{$item}";
$session = $_SESSION[$key];
$time = date('U');
$limit_end = $time + $seconds;

if (isset($session) && is_array($session) && count($session) === 2) {
[$count, $expires] = $session;
$diff = $expires - $time;

if (++$count >= $limit && $diff > 0) {
$validator->addError([
'field' => $item,
'rule' => self::RATE_LIMIT,
'fallback' => "$item has reached the rate limit which expires in $diff seconds.",
'meta' => ['expires' => $diff],
]);
break;
}

if ($diff <= 0) {
// Reset
$_SESSION[$key] = [1, $limit_end];
break;
}

$_SESSION[$key] = [$count, $expires];
} else {
$_SESSION[$key] = [1, $limit_end];
}

break;
}
}
}
Expand Down Expand Up @@ -379,7 +430,7 @@ public function errors(): array {
// Loop all errors to convert and get their custom messages
foreach ($this->_to_convert as $error) {

$message = $this->getMessage($error['field'], $error['rule'], $error['fallback']);
$message = $this->getMessage($error['field'], $error['rule'], $error['fallback'], $error['meta']);

// If there is no generic `message()` set or the translated message is not equal to generic message
// we can continue without worrying about duplications
Expand Down Expand Up @@ -409,10 +460,11 @@ public function errors(): array {
* @param string $field name of field to search for.
* @param string $rule rule which check failed. should be from the constants defined above.
* @param string $fallback fallback default message if custom message and generic message are not supplied.
* @param ?array $meta optional meta to provide to message.
*
* @return string Message for this field and rule.
*/
private function getMessage(string $field, string $rule, string $fallback): string {
private function getMessage(string $field, string $rule, string $fallback, ?array $meta = []): string {

// No custom messages defined for this field
if (!isset($this->_messages[$field])) {
Expand All @@ -436,6 +488,11 @@ private function getMessage(string $field, string $rule, string $fallback): stri
return $this->_message ?? $fallback;
}

// If the message is a callback function, provide it with meta
if (is_callable($this->_messages[$field][$rule])) {
return $this->_messages[$field][$rule]($meta);
}

// Rule-specific custom message was supplied
return $this->_messages[$field][$rule];
}
Expand Down
1 change: 1 addition & 0 deletions custom/languages/en_UK.json
Expand Up @@ -798,6 +798,7 @@
"general/previous": "Previous",
"general/privacy_policy": "Privacy Policy",
"general/profile": "Profile",
"general/rate_limit": "Please try again in {{expires}} seconds",
"general/register": "Register",
"general/remove": "Remove",
"general/report": "Report",
Expand Down
14 changes: 10 additions & 4 deletions modules/Core/pages/login.php
Expand Up @@ -54,12 +54,15 @@
unset($_SESSION['remember'], $_SESSION['password'], $_SESSION['tfa']);
}

$rate_limit = [5, 60]; // 5 attempts in 60 seconds - TODO allow this to be customised?
if ($login_method == 'email') {
$to_validate = [
'email' => [
Validate::REQUIRED => true,
Validate::IS_BANNED => true,
Validate::IS_ACTIVE => true
Validate::IS_ACTIVE => true,
Validate::RATE_LIMIT => $rate_limit,
],
'password' => [
Validate::REQUIRED => true
Expand All @@ -70,7 +73,8 @@
'username' => [
Validate::REQUIRED => true,
Validate::IS_BANNED => true,
Validate::IS_ACTIVE => true
Validate::IS_ACTIVE => true,
Validate::RATE_LIMIT => $rate_limit,
],
'password' => [
Validate::REQUIRED => true
Expand All @@ -82,12 +86,14 @@
'email' => [
Validate::REQUIRED => $language->get('user', 'must_input_email'),
Validate::IS_BANNED => $language->get('user', 'account_banned'),
Validate::IS_ACTIVE => $language->get('user', 'inactive_account')
Validate::IS_ACTIVE => $language->get('user', 'inactive_account'),
Validate::RATE_LIMIT => fn($meta) => $language->get('general', 'rate_limit', $meta),
],
'username' => [
Validate::REQUIRED => ($login_method == 'username' ? $language->get('user', 'must_input_username') : $language->get('user', 'must_input_email_or_username')),
Validate::IS_BANNED => $language->get('user', 'account_banned'),
Validate::IS_ACTIVE => $language->get('user', 'inactive_account')
Validate::IS_ACTIVE => $language->get('user', 'inactive_account'),
Validate::RATE_LIMIT => fn($meta) => $language->get('general', 'rate_limit', $meta),
],
'password' => $language->get('user', 'must_input_password')
]);
Expand Down

1 comment on commit 98fe4b7

@agnihackers
Copy link

Choose a reason for hiding this comment

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

@admin maintainer as given the permission for assigning CVE. So please assign a CVE for this report

Please sign in to comment.