Merge pull request #9497 from JakubOnderka/rate-limit-fix

fix: [internal] Rate limiting
pull/9498/head
Jakub Onderka 2024-01-14 20:45:45 +01:00 committed by GitHub
commit b4602e74f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 69 deletions

View File

@ -830,29 +830,34 @@ class AppController extends Controller
private function __rateLimitCheck(array $user)
{
$info = array();
$rateLimitCheck = $this->RateLimit->check(
$user,
$this->request->params['controller'],
$this->request->action,
$info,
$this->response->type()
$this->request->params['action'],
);
if (!empty($info)) {
$this->RestResponse->setHeader('X-Rate-Limit-Limit', $info['limit']);
$this->RestResponse->setHeader('X-Rate-Limit-Remaining', $info['remaining']);
$this->RestResponse->setHeader('X-Rate-Limit-Reset', $info['reset']);
if ($rateLimitCheck) {
$headers = [
'X-Rate-Limit-Limit' => $rateLimitCheck['limit'],
'X-Rate-Limit-Remaining' => $rateLimitCheck['remaining'],
'X-Rate-Limit-Reset' => $rateLimitCheck['reset'],
];
if ($rateLimitCheck['exceeded']) {
$response = $this->RestResponse->throwException(
429,
__('Rate limit exceeded.'),
'/' . $this->request->params['controller'] . '/' . $this->request->params['action'],
false,
false,
$headers
);
$response->send();
$this->_stop();
} else {
$this->RestResponse->headers = array_merge($this->RestResponse->headers, $headers);
}
}
if ($rateLimitCheck !== true) {
$this->response->header('X-Rate-Limit-Limit', $info['limit']);
$this->response->header('X-Rate-Limit-Remaining', $info['remaining']);
$this->response->header('X-Rate-Limit-Reset', $info['reset']);
$this->response->body($rateLimitCheck);
$this->response->statusCode(429);
$this->response->send();
$this->_stop();
}
return true;
}
public function afterFilter()

View File

@ -12,58 +12,60 @@ class RateLimitComponent extends Component
)
);
public $components = array('RestResponse');
/**
* @param array $user
* @param string $controller
* @param string $action
* @param array $info
* @param string $responseType
* @return bool
* @return array|null
* @throws RedisException
*/
public function check(array $user, $controller, $action, &$info = array(), $responseType)
public function check(array $user, $controller, $action)
{
if (!empty($user['Role']['enforce_rate_limit']) && isset(self::LIMITED_FUNCTIONS[$controller][$action])) {
if ($user['Role']['rate_limit_count'] == 0) {
throw new MethodNotAllowedException(__('API searches are not allowed for this user role.'));
}
try {
$redis = RedisTool::init();
} catch (Exception $e) {
return true; // redis is not available, allow access
}
$uuid = Configure::read('MISP.uuid') ?: 'no-uuid';
$keyName = 'misp:' . $uuid . ':rate_limit:' . $user['id'];
$count = $redis->get($keyName);
if ($count !== false && $count >= $user['Role']['rate_limit_count']) {
$info = array(
'limit' => $user['Role']['rate_limit_count'],
'reset' => $redis->ttl($keyName),
'remaining' => $user['Role']['rate_limit_count'] - $count,
);
return $this->RestResponse->throwException(
429,
__('Rate limit exceeded.'),
'/' . $controller . '/' . $action,
$responseType
);
} else {
if ($count === false) {
$redis->setEx($keyName, 900, 1);
} else {
$redis->setEx($keyName, $redis->ttl($keyName), intval($count) + 1);
}
}
$count += 1;
$info = array(
'limit' => $user['Role']['rate_limit_count'],
'reset' => $redis->ttl($keyName),
'remaining' => $user['Role']['rate_limit_count'] - $count
);
if (!isset(self::LIMITED_FUNCTIONS[$controller][$action])) {
return null; // no limit enforced for this controller action
}
return true;
if (empty($user['Role']['enforce_rate_limit'])) {
return null; // no limit enforced for this role
}
$rateLimit = (int)$user['Role']['rate_limit_count'];
if ($rateLimit === 0) {
throw new MethodNotAllowedException(__('API searches are not allowed for this user role.'));
}
try {
$redis = RedisTool::init();
} catch (Exception $e) {
return null; // redis is not available, allow access
}
$uuid = Configure::read('MISP.uuid') ?: 'no-uuid';
$keyName = 'misp:' . $uuid . ':rate_limit:' . $user['id'];
$count = $redis->get($keyName);
if ($count !== false && $count >= $rateLimit) {
return [
'exceeded' => true,
'limit' => $rateLimit,
'reset' => $redis->ttl($keyName),
'remaining' => $rateLimit - $count,
];
}
$newCount = $redis->incr($keyName);
if ($newCount === 1) {
$redis->expire($keyName, 900);
$reset = 900;
} else {
$reset = $redis->ttl($keyName);
}
return [
'exceeded' => false,
'limit' => $rateLimit,
'reset' => $reset,
'remaining' => $rateLimit - $newCount,
];
}
}

View File

@ -517,7 +517,7 @@ class RestResponseComponent extends Component
if ($id) {
$response['id'] = $id;
}
return $this->__sendResponse($response, 403, $format);
return $this->prepareResponse($response, 403, $format);
}
/**
@ -562,7 +562,7 @@ class RestResponseComponent extends Component
if ($id) {
$response['id'] = $id;
}
return $this->__sendResponse($response, 200, $format);
return $this->prepareResponse($response, 200, $format);
}
/**
@ -587,7 +587,7 @@ class RestResponseComponent extends Component
* @return CakeResponse
* @throws Exception
*/
private function __sendResponse($response, $code, $format = false, $raw = false, $download = false, $headers = array())
private function prepareResponse($response, $code, $format = false, $raw = false, $download = false, $headers = array())
{
App::uses('TmpFileTool', 'Tools');
$format = !empty($format) ? strtolower($format) : 'json';
@ -775,7 +775,7 @@ class RestResponseComponent extends Component
if (!empty($errors)) {
$data['errors'] = $errors;
}
return $this->__sendResponse($data, 200, $format, $raw, $download, $headers);
return $this->prepareResponse($data, 200, $format, $raw, $download, $headers);
}
/**
@ -807,7 +807,7 @@ class RestResponseComponent extends Component
'message' => $message,
'url' => $url
);
return $this->__sendResponse($message, $code, $format, $raw, false, $headers);
return $this->prepareResponse($message, $code, $format, $raw, false, $headers);
}
public function setHeader($header, $value)
@ -834,7 +834,7 @@ class RestResponseComponent extends Component
}
}
$response['url'] = $this->__generateURL($actionArray, $controller, $params);
return $this->__sendResponse($response, 200, $format);
return $this->prepareResponse($response, 200, $format);
}
private function __setup()