From 7547d00bb4f1bf05186c47ba1eb6d1ba93b074c9 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 22 Oct 2026 14:45:04 +0530 Subject: [PATCH] feat(security): place login throttle before CSRF and add visual CAPTCHA verification to login form --- app/Config/Filters.php | 1 + app/Config/Routes.php | 1 + app/Controllers/Captcha.php | 88 +++++++++++++++++++++++++++++ app/Filters/LoginThrottleFilter.php | 49 ++++++++++++---- app/Views/auth/login.php | 39 +++++++++++++ 5 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 app/Controllers/Captcha.php diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 6d04171..f1ce0c7 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -79,6 +79,7 @@ class Filters extends BaseFilters */ public array $globals = [ 'before' => [ + 'loginThrottle', 'singleSession', 'invalidchars', 'csrf' => [ diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d94dd64..d0b45da 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -376,6 +376,7 @@ $routes->post('Finance/bank_pos_settlement_create', 'Finance::bankPosSettlementC $routes->post('Finance/bank_pos_settlement_accept', 'Finance::bankPosSettlementAccept'); service('auth')->routes($routes); +$routes->get('captcha', 'Captcha::generate'); $routes->get('auth/session-status', 'AuthSession::status'); // Hospital Stock Management (Storestock) diff --git a/app/Controllers/Captcha.php b/app/Controllers/Captcha.php new file mode 100644 index 0000000..6f92380 --- /dev/null +++ b/app/Controllers/Captcha.php @@ -0,0 +1,88 @@ +set('login_captcha', $code); + + // 3. Create GD image + $width = 140; + $height = 40; + $image = imagecreatetruecolor($width, $height); + + // Background color (#f8fafc) + $bgColor = imagecolorallocate($image, 248, 250, 252); + imagefilledrectangle($image, 0, 0, $width, $height, $bgColor); + + // Add subtle background noise dots + for ($i = 0; $i < 60; $i++) { + $dotColor = imagecolorallocate($image, random_int(190, 230), random_int(190, 230), random_int(200, 240)); + imagesetpixel($image, random_int(0, $width - 1), random_int(0, $height - 1), $dotColor); + } + + // Add 3 subtle noise lines + for ($i = 0; $i < 3; $i++) { + $lineColor = imagecolorallocate($image, random_int(180, 220), random_int(180, 220), random_int(190, 230)); + imageline($image, random_int(0, $width), random_int(0, $height), random_int(0, $width), random_int(0, $height), $lineColor); + } + + // Color palette for characters + $colors = [ + imagecolorallocate($image, 15, 23, 42), // slate-900 + imagecolorallocate($image, 30, 58, 138), // blue-900 + imagecolorallocate($image, 88, 28, 135), // purple-900 + imagecolorallocate($image, 19, 78, 74), // teal-900 + imagecolorallocate($image, 136, 19, 55), // rose-900 + ]; + + // Draw each character with spacing and slight vertical jitter + $spacing = (int) (($width - 24) / $length); + for ($i = 0; $i < $length; $i++) { + $char = $code[$i]; + $charColor = $colors[$i % count($colors)]; + $x = 12 + ($i * $spacing); + $y = random_int(10, 14); + + imagestring($image, 5, $x, $y, $char, $charColor); + // Draw 1px horizontal offset for bold readability + imagestring($image, 5, $x + 1, $y, $char, $charColor); + } + + // Add a thin border + $borderColor = imagecolorallocate($image, 203, 213, 225); + imagerectangle($image, 0, 0, $width - 1, $height - 1, $borderColor); + + // Capture PNG stream + ob_start(); + imagepng($image); + $imageData = (string) ob_get_clean(); + imagedestroy($image); + + return $this->response + ->setContentType('image/png') + ->setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') + ->setHeader('Pragma', 'no-cache') + ->setHeader('Expires', '0') + ->setBody($imageData); + } +} diff --git a/app/Filters/LoginThrottleFilter.php b/app/Filters/LoginThrottleFilter.php index 3e1a755..9db069e 100644 --- a/app/Filters/LoginThrottleFilter.php +++ b/app/Filters/LoginThrottleFilter.php @@ -10,13 +10,15 @@ use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; /** - * Authentication Endpoint Rate Limiting Filter (WASA CWE-307 Compliant) + * Authentication Endpoint Rate Limiting & CAPTCHA Filter (WASA CWE-307 Compliant) * - * Enforces server-side rate limiting on authentication attempts: - * - Restricts requests to a maximum of 5 attempts per 60 seconds per IP address. - * - When threshold is exceeded, returns HTTP 429 Too Many Requests. - * - Emits standard Retry-After and X-RateLimit-* headers. - * - Logs violations for security monitoring and incident response. + * Enforces dual-layer defense on authentication endpoints: + * 1. Server-side Rate Limiting (Token Bucket): + * - Restricts login POST requests to a maximum of 5 attempts per 60 seconds per IP address. + * - When threshold is exceeded, returns HTTP 429 Too Many Requests with Retry-After header. + * - Logs security events for SIEM / audit monitoring. + * 2. Visual Security Verification (CAPTCHA): + * - Validates human presence to block automated credential brute-force, password spraying, and bots. */ class LoginThrottleFilter implements FilterInterface { @@ -40,7 +42,16 @@ class LoginThrottleFilter implements FilterInterface return; } - // Only throttle state-changing authentication attempts (POST) + $uri = trim($request->getUri()->getPath(), '/'); + // Only target authentication endpoints + $authRoutes = ['login', 'login/magic-link', 'register']; + $isAuthRoute = in_array($uri, $authRoutes, true) || str_starts_with($uri, 'login/') || str_starts_with($uri, 'register/'); + + if (! $isAuthRoute) { + return; + } + + // Only throttle state-changing requests (POST) if (strtoupper($request->getMethod()) !== 'POST') { return; } @@ -49,7 +60,7 @@ class LoginThrottleFilter implements FilterInterface $ip = $request->getIPAddress(); $throttleKey = 'auth_throttle_' . md5($ip); - // Check if the request exceeds capacity + // 1. Check IP rate limit (Token Bucket) if ($throttler->check($throttleKey, $this->maxAttempts, $this->decaySeconds, 1) === false) { $retryAfter = max(1, $throttler->getTokenTime()); $resetTime = time() + $retryAfter; @@ -57,7 +68,7 @@ class LoginThrottleFilter implements FilterInterface // Log security warning for SIEM / audit monitoring log_message( 'warning', - "[WASA-CWE-307] Authentication rate limit exceeded. IP: {$ip}, Path: {$request->getUri()->getPath()}, Retry-After: {$retryAfter}s" + "[WASA-CWE-307] Authentication rate limit exceeded. IP: {$ip}, Path: {$uri}, Retry-After: {$retryAfter}s" ); /** @var ResponseInterface $response */ @@ -80,7 +91,7 @@ class LoginThrottleFilter implements FilterInterface ]); } - // Return HTML response for standard browser form submissions + // Return HTML response for standard browser submissions $body = '' . '429 Too Many Requests' . '' @@ -104,6 +115,24 @@ class LoginThrottleFilter implements FilterInterface return $response->setBody($body); } + + // 2. Validate CAPTCHA for standard login POST + if ($uri === 'login' || $uri === 'login/') { + $submittedCaptcha = trim((string) $request->getPost('captcha')); + $expectedCaptcha = (string) session('login_captcha'); + + if ($expectedCaptcha === '' || strcasecmp($submittedCaptcha, $expectedCaptcha) !== 0) { + // Invalidate session captcha so it cannot be retried + session()->remove('login_captcha'); + + return redirect()->route('login') + ->withInput() + ->with('error', 'Invalid Security Verification (CAPTCHA) code. Please enter the characters shown in the image.'); + } + + // Consume captcha to prevent replay attacks + session()->remove('login_captcha'); + } } /** diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php index 4662fb1..9a51969 100644 --- a/app/Views/auth/login.php +++ b/app/Views/auth/login.php @@ -92,6 +92,45 @@
Please enter your password!
+ +
+
+ + +
+
+ + Security CAPTCHA + + +
Please enter the security verification characters.
+
+
+ + +