From e85773601763b7fcc08e1c4c8eb9c6a509fd872a Mon Sep 17 00:00:00 2001 From: Hossein Azizabadi Farahani Date: Mon, 4 Nov 2024 12:07:09 +0330 Subject: [PATCH] Move security checks to core module --- config/module.config.php | 2 - .../AuthenticationMiddlewareFactory.php | 2 +- src/Factory/Security/AccountLockedFactory.php | 34 --- .../Security/AccountLoginAttemptsFactory.php | 36 --- src/Factory/Service/AccountServiceFactory.php | 4 +- src/Middleware/AuthenticationMiddleware.php | 2 +- src/Middleware/SecurityMiddleware.php | 20 +- src/Security/Account/AccountLocked.php | 131 ----------- src/Security/Account/AccountLoginAttempts.php | 104 --------- .../Account/AccountSecurityInterface.php | 16 -- src/Security/Request/Injection.php | 155 ------------- src/Security/Request/InputSizeLimit.php | 75 ------- src/Security/Request/InputValidation.php | 189 ---------------- src/Security/Request/Ip.php | 209 ------------------ src/Security/Request/Method.php | 63 ------ src/Security/Request/RequestLimit.php | 131 ----------- .../Request/RequestSecurityInterface.php | 26 --- src/Security/Request/Xss.php | 138 ------------ src/Security/Response/Compress.php | 52 ----- src/Security/Response/Escape.php | 63 ------ src/Security/Response/Headers.php | 69 ------ .../Response/ResponseSecurityInterface.php | 16 -- src/Service/AccountService.php | 4 +- 23 files changed, 16 insertions(+), 1525 deletions(-) delete mode 100644 src/Factory/Security/AccountLockedFactory.php delete mode 100644 src/Factory/Security/AccountLoginAttemptsFactory.php delete mode 100644 src/Security/Account/AccountLocked.php delete mode 100644 src/Security/Account/AccountLoginAttempts.php delete mode 100644 src/Security/Account/AccountSecurityInterface.php delete mode 100644 src/Security/Request/Injection.php delete mode 100644 src/Security/Request/InputSizeLimit.php delete mode 100644 src/Security/Request/InputValidation.php delete mode 100644 src/Security/Request/Ip.php delete mode 100644 src/Security/Request/Method.php delete mode 100644 src/Security/Request/RequestLimit.php delete mode 100644 src/Security/Request/RequestSecurityInterface.php delete mode 100644 src/Security/Request/Xss.php delete mode 100644 src/Security/Response/Compress.php delete mode 100644 src/Security/Response/Escape.php delete mode 100644 src/Security/Response/Headers.php delete mode 100644 src/Security/Response/ResponseSecurityInterface.php diff --git a/config/module.config.php b/config/module.config.php index 42969a2..2c9a97c 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -47,8 +47,6 @@ Service\ExportService::class => Factory\Service\ExportServiceFactory::class, Service\TranslatorService::class => Factory\Service\TranslatorServiceFactory::class, Service\InstallerService::class => Factory\Service\InstallerServiceFactory::class, - Security\Account\AccountLoginAttempts::class => Factory\Security\AccountLoginAttemptsFactory::class, - Security\Account\AccountLocked::class => Factory\Security\AccountLockedFactory::class, Handler\Admin\Profile\AddHandler::class => Factory\Handler\Admin\Profile\AddHandlerFactory::class, Handler\Admin\Profile\EditHandler::class => Factory\Handler\Admin\Profile\EditHandlerFactory::class, Handler\Admin\Profile\ListHandler::class => Factory\Handler\Admin\Profile\ListHandlerFactory::class, diff --git a/src/Factory/Middleware/AuthenticationMiddlewareFactory.php b/src/Factory/Middleware/AuthenticationMiddlewareFactory.php index 9c23d3e..76db067 100644 --- a/src/Factory/Middleware/AuthenticationMiddlewareFactory.php +++ b/src/Factory/Middleware/AuthenticationMiddlewareFactory.php @@ -2,6 +2,7 @@ namespace User\Factory\Middleware; +use Core\Security\Account\AccountLocked; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; @@ -10,7 +11,6 @@ use Psr\Http\Message\StreamFactoryInterface; use User\Handler\ErrorHandler; use User\Middleware\AuthenticationMiddleware; -use User\Security\Account\AccountLocked; use User\Service\AccountService; use User\Service\CacheService; use User\Service\TokenService; diff --git a/src/Factory/Security/AccountLockedFactory.php b/src/Factory/Security/AccountLockedFactory.php deleted file mode 100644 index 6b0ae6c..0000000 --- a/src/Factory/Security/AccountLockedFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -get('config'); - $config = $config['security'] ?? []; - - return new AccountLocked( - $container->get(CacheService::class), - $config - ); - } -} \ No newline at end of file diff --git a/src/Factory/Security/AccountLoginAttemptsFactory.php b/src/Factory/Security/AccountLoginAttemptsFactory.php deleted file mode 100644 index 6c046cf..0000000 --- a/src/Factory/Security/AccountLoginAttemptsFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -get('config'); - $config = $config['security'] ?? []; - - return new AccountLoginAttempts( - $container->get(CacheService::class), - $container->get(AccountLocked::class), - $config - ); - } -} \ No newline at end of file diff --git a/src/Factory/Service/AccountServiceFactory.php b/src/Factory/Service/AccountServiceFactory.php index 7ef775f..2489fbc 100644 --- a/src/Factory/Service/AccountServiceFactory.php +++ b/src/Factory/Service/AccountServiceFactory.php @@ -2,14 +2,14 @@ namespace User\Factory\Service; +use Core\Security\Account\AccountLocked; +use Core\Security\Account\AccountLoginAttempts; use Laminas\ServiceManager\Factory\FactoryInterface; use Notification\Service\NotificationService; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use User\Repository\AccountRepositoryInterface; -use User\Security\Account\AccountLocked; -use User\Security\Account\AccountLoginAttempts; use User\Service\AccountService; use User\Service\AvatarService; use User\Service\CacheService; diff --git a/src/Middleware/AuthenticationMiddleware.php b/src/Middleware/AuthenticationMiddleware.php index b9f2a50..7c90574 100644 --- a/src/Middleware/AuthenticationMiddleware.php +++ b/src/Middleware/AuthenticationMiddleware.php @@ -2,6 +2,7 @@ namespace User\Middleware; +use Core\Security\Account\AccountLocked; use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; @@ -10,7 +11,6 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use User\Handler\ErrorHandler; -use User\Security\Account\AccountLocked; use User\Service\AccountService; use User\Service\CacheService; use User\Service\TokenService; diff --git a/src/Middleware/SecurityMiddleware.php b/src/Middleware/SecurityMiddleware.php index 5370545..95d8549 100644 --- a/src/Middleware/SecurityMiddleware.php +++ b/src/Middleware/SecurityMiddleware.php @@ -2,6 +2,16 @@ namespace User\Middleware; +use Core\Security\Request\Injection as RequestSecurityInjection; +use Core\Security\Request\InputSizeLimit as RequestSecurityInputSizeLimit; +use Core\Security\Request\InputValidation as RequestSecurityInputValidation; +use Core\Security\Request\Ip as RequestSecurityIp; +use Core\Security\Request\Method as RequestSecurityMethod; +use Core\Security\Request\RequestLimit as RequestSecurityRequestLimit; +use Core\Security\Request\Xss as RequestSecurityXss; +use Core\Security\Response\Compress as ResponseCompress; +use Core\Security\Response\Escape as ResponseEscape; +use Core\Security\Response\Headers as ResponseHeaders; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -9,16 +19,6 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use User\Handler\ErrorHandler; -use User\Security\Request\Injection as RequestSecurityInjection; -use User\Security\Request\InputSizeLimit as RequestSecurityInputSizeLimit; -use User\Security\Request\InputValidation as RequestSecurityInputValidation; -use User\Security\Request\Ip as RequestSecurityIp; -use User\Security\Request\Method as RequestSecurityMethod; -use User\Security\Request\RequestLimit as RequestSecurityRequestLimit; -use User\Security\Request\Xss as RequestSecurityXss; -use User\Security\Response\Compress as ResponseCompress; -use User\Security\Response\Escape as ResponseEscape; -use User\Security\Response\Headers as ResponseHeaders; use User\Service\CacheService; use User\Service\UtilityService; diff --git a/src/Security/Account/AccountLocked.php b/src/Security/Account/AccountLocked.php deleted file mode 100644 index 2122bf0..0000000 --- a/src/Security/Account/AccountLocked.php +++ /dev/null @@ -1,131 +0,0 @@ -cacheService = $cacheService; - $this->config = $config; - } - - /** - * @param array $params - * - * @return void - */ - public function doLocked(array $params): void - { - // ip in whitelist - $inWhitelist = $params['security_stream']['ip']['data']['in_whitelist'] ?? false; - - // Set key - switch ($params['type']) { - default: - case 'id': - $keyLocked = "locked_account_{$params['user_id']}"; - break; - - case 'ip': - if (!$inWhitelist) { - $keyLocked = $this->sanitizeKey("locked_ip_{$params['user_ip']}"); - } - break; - } - - // do lock - if (isset($keyLocked)) { - $this->cacheService->setItem( - $keyLocked, - ['locked_from' => time(), 'locked_to' => time() + $this->config['account']['ttl']], - $this->config['account']['ttl'] - ); - } - } - - /** - * @param array $params - * - * @return bool - */ - public function isLocked(array $params): bool - { - // Set key - switch ($params['type']) { - default: - case 'id': - $keyLocked = "locked_account_{$params['user_id']}"; - break; - - case 'ip': - $userIp = $params['user_ip'] ?? $_SERVER['REMOTE_ADDR']; - $keyLocked = $this->sanitizeKey("locked_ip_{$userIp}"); - break; - } - - // Check is locked - if ($this->cacheService->hasItem($keyLocked)) { - return true; - } - - return false; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - $ttl = $this->config['account']['ttl']; - if ($ttl < 3600) { - $minutes = floor(($ttl % 3600) / 60); - $message = sprintf('Access denied: Your account is locked due to too many failed login attempts. Please try again after %s minutes.', $minutes); - } elseif ($ttl < 86400) { - $hours = floor(($ttl % 86400) / 3600); - $message = sprintf('Access denied: Your account is locked due to too many failed login attempts. Please try again after %s hours.', $hours); - } else { - $days = floor($ttl / 86400); - $hours = floor(($ttl % 86400) / 3600); - $message = sprintf( - 'Access denied: Your account is locked due to too many failed login attempts. Please try again after %s days, %s hours.', $days, $hours - ); - } - - return $message; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_UNAUTHORIZED; - } - - /** - * Sanitizes the cache key to ensure it meets the allowed format. - * - * @param string $key The original key - * - * @return string The sanitized key - */ - private function sanitizeKey(string $key): string - { - return preg_replace('/[^a-zA-Z0-9_]/', '_', $key); - } -} \ No newline at end of file diff --git a/src/Security/Account/AccountLoginAttempts.php b/src/Security/Account/AccountLoginAttempts.php deleted file mode 100644 index 45593b1..0000000 --- a/src/Security/Account/AccountLoginAttempts.php +++ /dev/null @@ -1,104 +0,0 @@ -cacheService = $cacheService; - $this->accountLocked = $accountLocked; - $this->config = $config; - } - - /** - * @param array $params - * - * @return array - */ - public function incrementFailedAttempts(array $params): array - { - // Set key - switch ($params['type']) { - default: - case 'id': - $keyAttempts = "account_login_attempts_{$params['user_id']}"; - break; - - case 'ip': - $keyAttempts = $this->sanitizeKey("ip_login_attempts_{$params['user_ip']}"); - break; - } - - // Check account is lock or not - if ($this->accountLocked->isLocked($params)) { - return [ - 'can_try' => false, - ]; - } - - // Get and update attempts - $attempts = $this->cacheService->getItem($keyAttempts); - if (empty($attempts)) { - $attempts = $this->cacheService->setItem($keyAttempts, ['count' => 1], $this->config['account']['ttl']); - } else { - $attempts = $this->cacheService->setItem($keyAttempts, ['count' => $attempts['count'] + 1], $this->config['account']['ttl']); - } - - if ($attempts['count'] >= $this->config['account']['attempts']) { - $this->accountLocked->doLocked($params); - } - - return [ - 'can_try' => true, - 'attempts_count' => $attempts['count'], - 'attempts_remind' => $this->config['account']['attempts'] - $attempts['count'], - ]; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Your account is locked due to too many failed login attempts. Please try again after 1 hour.'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_UNAUTHORIZED; - } - - /** - * Sanitizes the cache key to ensure it meets the allowed format. - * - * @param string $key The original key - * - * @return string The sanitized key - */ - private function sanitizeKey(string $key): string - { - return preg_replace('/[^a-zA-Z0-9_]/', '_', $key); - } -} \ No newline at end of file diff --git a/src/Security/Account/AccountSecurityInterface.php b/src/Security/Account/AccountSecurityInterface.php deleted file mode 100644 index b698514..0000000 --- a/src/Security/Account/AccountSecurityInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Check if the IP is in the whitelist - if ( - (bool)$this->config['injection']['ignore_whitelist'] === true - && isset($securityStream['ip']['data']['in_whitelist']) - && (bool)$securityStream['ip']['data']['in_whitelist'] === true - ) { - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'ignore', - 'data' => [], - ]; - } - - // Get request and query body - $requestParams = $request->getParsedBody(); - $QueryParams = $request->getQueryParams(); - $params = array_merge($requestParams, $QueryParams); - - // Do check - if (!empty($params)) { - if ($this->detectInjection($params)) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Injection detected'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } - - private function detectInjection($input): bool - { - $injectionPatterns = [ - // Basic SQL keywords, scoped to query-like structures - '/\b(select|insert|update|delete|drop|create|alter|truncate|exec|execute|grant|revoke|declare)\b\s+[\w\(\*]/i', - - // SQL comments and operators, scoped to likely SQL contexts - '/(--|\#)/', // SQL single-line comment or # as comment - '/\/\*.*?\*\//s', // SQL multi-line comment - '/\b(or|and)\s+1\s*=\s*1\b/i', // Boolean conditions - '/\b(or|and)\s+\'[^\']{1,255}\'\s*=\s*\'[^\']{1,255}\'/i', // String comparisons (limit length to avoid matching long tokens) - '/\b(or|and)\s+\"[^\"]{1,255}\"\s*=\s*\"[^\"]{1,255}\"/i', // Double-quoted string comparisons (same limit) - - // SQL functions and expressions, scoped to query-like patterns - '/\b(select|insert|update|delete|exec|execute|db_name|user|version|ifnull|sleep|benchmark)\b\s*\(/i', // Functions with opening parentheses - '/\binformation_schema\b/i', // Targeting database metadata - '/\bcase\s+when\b/i', // Case statements - '/\bnull\b/i', // Handling null values - - // Unions and conditional operations, scoped to query-like structures - '/\bunion\b\s+select\b/i', // Union select pattern - '/\bunion\s+all\b\s+select\b/i', // Union all select pattern - '/\bexists\s*\(\s*select\b/i', // Checking for subquery existence - - // Other suspicious characters, scoped to SQL contexts - '/;/', // Statement terminators - '/\bcast\b\s*\(/i', // Cast functions - '/\bconvert\b\s*\(/i', // Convert functions - '/\bdrop\s+database\b/i', // Dropping databases - '/\bshutdown\b/i', // Attempting to shut down the database - '/\bwaitfor\s+delay\b/i', // Time delay operations - - // Hex or binary injection with limits to avoid false positives - '/\b0x[0-9a-fA-F]{2,255}\b/i', // Hexadecimal injection (limit length) - '/\bx\'[0-9a-fA-F]{2,255}\'/i', // Hex-encoded strings (limit length) - '/\b(b|x)[\'"]?[0-9a-fA-F]{2,255}[\'"]?/i', // Binary/hex literals (same adjustment) - - // Miscellaneous suspicious patterns, scoped more contextually - '/\b(select.*from|union.*select|insert.*into|update.*set|delete\s+from|drop\s+table|create\s+table|alter\s+table|truncate\s+table)\b/i', - - // Catching numeric or chained conditions in SQL injection - '/\b(or|and)\s+\d{1,255}\s*=\s*\d{1,255}/i', // Numeric equality checks (limit length) - '/\b(?:like|regexp)\b/i', // Check for pattern matching (like or regexp) - '/\b(if|case)\s*\(/i', // If/Case conditions - '/\s*;\s*(select|insert|update|delete|drop|create|alter|truncate)\s+/i', // Chained queries - - // Handling encoded input - '/(?:%27|%22|%3D|%3B|%23|%2D|%2F|%5C)/i', // URL encoded equivalents of ', ", =, ;, #, -, /, and \ (common SQLi encodings) - ]; - - // If input is an array, recursively check each item - if (is_array($input)) { - foreach ($input as $value) { - if ($this->detectInjection($value)) { - return true; // SQL injection detected in one of the array items - } - } - return false; // No SQL injection detected in any array items - } - - // If input is a string, check for SQL injection patterns - if (is_string($input)) { - foreach ($injectionPatterns as $pattern) { - if (preg_match($pattern, $input)) { - return true; // SQL injection detected - } - } - } - - return false; // No SQL injection detected - } -} \ No newline at end of file diff --git a/src/Security/Request/InputSizeLimit.php b/src/Security/Request/InputSizeLimit.php deleted file mode 100644 index 4d875b6..0000000 --- a/src/Security/Request/InputSizeLimit.php +++ /dev/null @@ -1,75 +0,0 @@ -config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - if ($this->isLargeInput($request)) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - /** - * Checks if the request input exceeds the maximum allowed size. - * - * @param ServerRequestInterface $request - * - * @return bool - */ - private function isLargeInput(ServerRequestInterface $request): bool - { - $body = $request->getBody(); - $size = $body->getSize(); - return $size > $this->config['inputSizeLimit']['max_input_size']; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Input data is too large'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } -} \ No newline at end of file diff --git a/src/Security/Request/InputValidation.php b/src/Security/Request/InputValidation.php deleted file mode 100644 index 9fb05db..0000000 --- a/src/Security/Request/InputValidation.php +++ /dev/null @@ -1,189 +0,0 @@ -config = $config; - $this->inputFilter = new InputFilter(); - } - - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Check if the IP is in the whitelist - if ( - (bool)$this->config['inputValidation']['ignore_whitelist'] === true - && isset($securityStream['ip']['data']['in_whitelist']) - && (bool)$securityStream['ip']['data']['in_whitelist'] === true - ) { - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'ignore', - 'data' => [], - ]; - } - - // Get request and query body - $requestParams = $request->getParsedBody(); - $QueryParams = $request->getQueryParams(); - $params = array_merge($requestParams, $QueryParams); - - // Do check - if (!empty($params)) { - $this->processData($params); - $this->inputFilter->setData($params); - if (!$this->inputFilter->isValid()) { - // Set error message - $this->setErrorMessage($this->inputFilter->getMessages()); - - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [ - 'message' => $this->inputFilter->getMessages(), - ], - ]; - } - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - public function setErrorMessage($messages): void - { - if (is_string($messages)) { - $this->message = $this->message . ' ' . $messages; - } else { - $errorMessage = []; - foreach ($messages as $field => $filedMessage) { - if (is_array($filedMessage)) { - foreach ($filedMessage as $subField => $subFiledMessage) { - $errorMessage[$subField] = $field . ': ' . $subFiledMessage; - } - } else { - $errorMessage[$field] = $field . ': ' . $filedMessage; - } - } - - $this->message = $this->message . implode(', ', $errorMessage); - } - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return $this->message; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } - - private function processData(array $data): void - { - foreach ($data as $key => $value) { - if ($value !== null && $value !== '') { - continue; - } - - // Initialize the input - $input = new Input($key); - $input->getFilterChain()->attach(new StringTrim())->attach(new StripTags())->attach(new HtmlEntities()); - - // Handle different types of data - switch (gettype($value)) { - case 'integer': - $input->getValidatorChain()->attach(new Digits()); - break; - - case 'double': // Floats in PHP are of type 'double' - $input->getValidatorChain()->attach(new IsFloat()); - break; - - case 'string': - // Allow empty strings as valid inputs - $input->getValidatorChain()->attach(new StringLength(['min' => 0, 'max' => 65535])); - // Apply specific validators if applicable - if (filter_var($value, FILTER_VALIDATE_EMAIL)) { - $input->getValidatorChain()->attach(new EmailAddress()); - } elseif (filter_var($value, FILTER_VALIDATE_URL)) { - $input->getValidatorChain()->attach(new Uri()); - } elseif (filter_var($value, FILTER_VALIDATE_IP)) { - $input->getValidatorChain()->attach(new Ip()); - } elseif ($this->isJson($value)) { - $input->getValidatorChain()->attach(new IsJsonString()); - } - break; - - case 'array': - $this->processData((array)$value); - continue 2; // Continue outer loop - - default: - if ($value !== null && $value !== '') { - $input->getValidatorChain()->attach(new NotEmpty()); - $this->inputFilter->setData([$key => $value]); - $this->inputFilter->getMessages()[$key][] = "Key '{$key}' has an unrecognized type and cannot be empty."; - } - break; - } - - $this->inputFilter->add($input); - } - } - - private function isJson($string): bool - { - return is_string($string) && json_last_error() === JSON_ERROR_NONE && json_decode($string) !== null; - } -} \ No newline at end of file diff --git a/src/Security/Request/Ip.php b/src/Security/Request/Ip.php deleted file mode 100644 index 1bf1f2f..0000000 --- a/src/Security/Request/Ip.php +++ /dev/null @@ -1,209 +0,0 @@ -cacheService = $cacheService; - $this->config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Get client ip - $clientIp = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; - - // Check ip is not lock - if ($this->isIpLocked($clientIp)) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - - // Check allow-list - if ($this->isWhitelist($clientIp)) { - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [ - 'client_ip' => $clientIp, - 'in_whitelist' => true, - ], - ]; - } - - // Check blacklist - if ($this->isBlacklisted($clientIp)) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [ - 'in_blacklisted' => true, - ], - ]; - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [ - 'client_ip' => $clientIp, - 'in_whitelist' => false, - ], - ]; - } - - /** - * Checks if the IP is in the allow-list. - * - * @param string $clientIp - * - * @return bool - */ - public function isIpLocked(string $clientIp): bool - { - $keyLocked = $this->sanitizeKey("locked_ip_{$clientIp}"); - if ($this->cacheService->hasItem($keyLocked)) { - return true; - } - return false; - } - - /** - * Checks if the IP is in the allow-list. - * - * @param string $clientIp - * - * @return bool - */ - public function isWhitelist(string $clientIp): bool - { - foreach ($this->config['ip']['whitelist'] as $entry) { - if ($this->ipMatches($clientIp, $entry)) { - return true; - } - } - return false; - } - - /** - * Checks if the IP is in the blacklist. - * - * @param string $clientIp - * - * @return bool - */ - public function isBlacklisted(string $clientIp): bool - { - foreach ($this->config['ip']['blacklist'] as $entry) { - if ($this->ipMatches($clientIp, $entry)) { - return true; - } - } - return false; - } - - /** - * Checks if an IP matches a given rule (single IP or range). - * - * @param string $clientIp - * @param string $rule - * - * @return bool - */ - private function ipMatches(string $clientIp, string $rule): bool - { - if (strpos($rule, '/') !== false) { - // Handle CIDR notation for IP ranges - return $this->ipInRange($clientIp, $rule); - } - - // Handle single IP addresses - return $clientIp === $rule; - } - - /** - * Checks if an IP is within a specified IP range. - * - * @param string $clientIp - * @param string $cidr - * - * @return bool - */ - private function ipInRange(string $clientIp, string $cidr): bool - { - [$range, $prefix] = explode('/', $cidr, 2); - $prefix = (int)$prefix; - - // Convert IP addresses to binary format - $clientIp = inet_pton($clientIp); - $range = inet_pton($range); - - // Calculate the subnet mask - $mask = str_repeat('1', $prefix) . str_repeat('0', 128 - $prefix); - $mask = pack('H*', str_pad(base_convert($mask, 2, 16), 32, '0', STR_PAD_LEFT)); - - // Apply the subnet mask to the range and IP - $range = $range & $mask; - $clientIp = $clientIp & $mask; - - return $range === $clientIp; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Bad IP'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } - - /** - * Sanitizes the cache key to ensure it meets the allowed format. - * - * @param string $key The original key - * - * @return string The sanitized key - */ - private function sanitizeKey(string $key): string - { - return preg_replace('/[^a-zA-Z0-9_]/', '_', $key); - } -} \ No newline at end of file diff --git a/src/Security/Request/Method.php b/src/Security/Request/Method.php deleted file mode 100644 index cb0b5d4..0000000 --- a/src/Security/Request/Method.php +++ /dev/null @@ -1,63 +0,0 @@ -config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Get request method - $method = $request->getMethod(); - if (!in_array($method, $this->config['method']['allow_method'] ?? ['POST'])) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Request method not allowed !'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } -} \ No newline at end of file diff --git a/src/Security/Request/RequestLimit.php b/src/Security/Request/RequestLimit.php deleted file mode 100644 index 37992bd..0000000 --- a/src/Security/Request/RequestLimit.php +++ /dev/null @@ -1,131 +0,0 @@ -cacheService = $cacheService; - $this->config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Check if the IP is in the whitelist - if ( - (bool)$this->config['requestLimit']['ignore_whitelist'] === true - && isset($securityStream['ip']['data']['in_whitelist']) - && (bool)$securityStream['ip']['data']['in_whitelist'] === true - ) { - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'ignore', - 'data' => [], - ]; - } - - // Get client ip - $clientIp = $request->getServerParams()['REMOTE_ADDR'] ?? 'unknown'; - - // Set key - $key = $this->sanitizeKey("rate_limit_{$clientIp}"); - - // Get and check key - $cacheData = $this->cacheService->getItem($key); - if (empty($cacheData)) { - $cacheData = ['count' => 1, 'timestamp' => time()]; - $this->cacheService->setItem($key, $cacheData, $this->config['requestLimit']['rate_limit']); - } else { - $cacheData = $this->validateCacheData($cacheData); - if ($cacheData === false) { - $cacheData = ['count' => 1, 'timestamp' => time()]; - } else { - // Update request count if within the rate limit window - if ($cacheData['count'] >= $this->config['requestLimit']['max_requests']) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - $cacheData['count'] += 1; - } - - $this->cacheService->setItem($key, $cacheData); - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - /** - * Sanitizes the cache key to ensure it meets the allowed format. - * - * @param string $key The original key - * - * @return string The sanitized key - */ - private function sanitizeKey(string $key): string - { - return preg_replace('/[^a-zA-Z0-9_]/', '_', $key); - } - - /** - * Validates the cache data to ensure it's still within the rate limit window. - * - * @param array $cacheData The cached data - * - * @return array|false Validated cache data or false if expired - */ - private function validateCacheData(array $cacheData): bool|array - { - if ((time() - $cacheData['timestamp']) > $this->config['requestLimit']['rate_limit']) { - return false; - } - return $cacheData; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: Rate limit exceeded. Please try again later'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } -} \ No newline at end of file diff --git a/src/Security/Request/RequestSecurityInterface.php b/src/Security/Request/RequestSecurityInterface.php deleted file mode 100644 index c51c45a..0000000 --- a/src/Security/Request/RequestSecurityInterface.php +++ /dev/null @@ -1,26 +0,0 @@ -config = $config; - } - - /** - * @param ServerRequestInterface $request - * @param array $securityStream - * - * @return array - */ - public function check(ServerRequestInterface $request, array $securityStream = []): array - { - // Check if the IP is in the whitelist - if ( - (bool)$this->config['xss']['ignore_whitelist'] === true - && isset($securityStream['ip']['data']['in_whitelist']) - && (bool)$securityStream['ip']['data']['in_whitelist'] === true - ) { - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'ignore', - 'data' => [], - ]; - } - - // Get request and query body - $requestParams = $request->getParsedBody(); - $QueryParams = $request->getQueryParams(); - $params = array_merge($requestParams, $QueryParams); - - // Do check - if (!empty($params)) { - if ($this->detectXSS($params)) { - return [ - 'result' => false, - 'name' => $this->name, - 'status' => 'unsuccessful', - 'data' => [], - ]; - } - } - - return [ - 'result' => true, - 'name' => $this->name, - 'status' => 'successful', - 'data' => [], - ]; - } - - /** - * @return string - */ - public function getErrorMessage(): string - { - return 'Access denied: XSS attack detected'; - } - - /** - * @return int - */ - public function getStatusCode(): int - { - return StatusCodeInterface::STATUS_BAD_REQUEST; - } - - private function detectXSS($input): bool - { - // Common XSS patterns - $xssPatterns = [ - '/]*>(.*?)<\/script>/is', // Detect