$ipAddress, 'timestamp' => time() - ($i * 30), // 30 seconds apart 'success' => false ]; } $isRateLimited = $this->checkRateLimit( ipAddress: $ipAddress, attempts: $attempts, maxAttempts: $maxAttempts, windowSeconds: $windowSeconds ); if (!$isRateLimited) { throw new \RuntimeException( 'Rate limiting not enforced after ' . count($attempts) . ' attempts' ); } echo "✅ Rate limiting enforced ({$maxAttempts} attempts per {$windowSeconds}s)\n"; } /** * Test account lockout after failed attempts */ public function testLocksAccountAfterFailedAttempts(): void { $username = 'testuser@example.com'; $maxFailedAttempts = 5; $lockoutDurationSeconds = 900; // 15 minutes // Simulate failed attempts $failedAttempts = []; for ($i = 0; $i < 6; $i++) { $failedAttempts[] = [ 'username' => $username, 'timestamp' => time() - ($i * 10), 'success' => false ]; } $lockoutInfo = $this->checkAccountLockout( username: $username, attempts: $failedAttempts, maxFailedAttempts: $maxFailedAttempts, lockoutDuration: $lockoutDurationSeconds ); if (!$lockoutInfo['is_locked']) { throw new \RuntimeException( 'Account not locked after ' . count($failedAttempts) . ' failed attempts' ); } echo "✅ Account locked after {$maxFailedAttempts} failed attempts\n"; echo " Lockout duration: {$lockoutDurationSeconds}s\n"; } /** * Test progressive delay on failed attempts */ public function testImplementsProgressiveDelay(): void { $attempts = [1, 2, 3, 4, 5]; $delays = []; foreach ($attempts as $attemptNumber) { $delay = $this->calculateProgressiveDelay($attemptNumber); $delays[$attemptNumber] = $delay; } // Verify delay increases with each attempt for ($i = 1; $i < count($delays); $i++) { if ($delays[$i] <= $delays[$i - 1]) { throw new \RuntimeException( "Progressive delay not increasing: attempt {$i} has delay {$delays[$i]}s, " . "previous was {$delays[$i-1]}s" ); } } echo "✅ Progressive delay implemented:\n"; foreach ($delays as $attempt => $delay) { echo " Attempt {$attempt}: {$delay}s delay\n"; } } /** * Test CAPTCHA requirement after suspicious activity */ public function testRequiresCaptchaAfterSuspiciousActivity(): void { $ipAddress = '203.0.113.42'; $failedAttempts = 3; $requiresCaptcha = $this->shouldRequireCaptcha( ipAddress: $ipAddress, failedAttempts: $failedAttempts, captchaThreshold: 3 ); if (!$requiresCaptcha) { throw new \RuntimeException( 'CAPTCHA not required after ' . $failedAttempts . ' failed attempts' ); } echo "✅ CAPTCHA required after {$failedAttempts} failed attempts\n"; } /** * Test distributed brute force detection */ public function testDetectsDistributedBruteForce(): void { $username = 'admin@example.com'; // Simulate attempts from multiple IPs $attempts = [ ['ip' => '203.0.113.1', 'username' => $username, 'timestamp' => time() - 100], ['ip' => '203.0.113.2', 'username' => $username, 'timestamp' => time() - 90], ['ip' => '203.0.113.3', 'username' => $username, 'timestamp' => time() - 80], ['ip' => '203.0.113.4', 'username' => $username, 'timestamp' => time() - 70], ['ip' => '203.0.113.5', 'username' => $username, 'timestamp' => time() - 60], ['ip' => '203.0.113.6', 'username' => $username, 'timestamp' => time() - 50], ]; $isDistributedAttack = $this->detectDistributedBruteForce( attempts: $attempts, username: $username, windowSeconds: 300, uniqueIpThreshold: 5 ); if (!$isDistributedAttack) { throw new \RuntimeException( 'Distributed brute force attack not detected (' . count($attempts) . ' attempts from different IPs)' ); } echo "✅ Distributed brute force attack detected\n"; echo " " . count($attempts) . " attempts from " . count(array_unique(array_column($attempts, 'ip'))) . " unique IPs\n"; } /** * Test password spray attack detection */ public function testDetectsPasswordSprayAttack(): void { // Simulate password spray: same password tried against multiple accounts $attempts = [ ['username' => 'user1@example.com', 'password' => 'Password123!', 'timestamp' => time() - 100], ['username' => 'user2@example.com', 'password' => 'Password123!', 'timestamp' => time() - 90], ['username' => 'user3@example.com', 'password' => 'Password123!', 'timestamp' => time() - 80], ['username' => 'user4@example.com', 'password' => 'Password123!', 'timestamp' => time() - 70], ['username' => 'user5@example.com', 'password' => 'Password123!', 'timestamp' => time() - 60], ]; $isPasswordSpray = $this->detectPasswordSpray( attempts: $attempts, windowSeconds: 300, uniqueUsernameThreshold: 5 ); if (!$isPasswordSpray) { throw new \RuntimeException( 'Password spray attack not detected (' . count($attempts) . ' attempts with same password)' ); } echo "✅ Password spray attack detected\n"; echo " Same password tried against " . count($attempts) . " different accounts\n"; } /** * Test successful login resets attempt counter */ public function testSuccessfulLoginResetsAttemptCounter(): void { $username = 'testuser@example.com'; // Failed attempts $attempts = [ ['username' => $username, 'success' => false, 'timestamp' => time() - 200], ['username' => $username, 'success' => false, 'timestamp' => time() - 150], ['username' => $username, 'success' => false, 'timestamp' => time() - 100], // Successful login ['username' => $username, 'success' => true, 'timestamp' => time() - 50], ]; $failedCount = $this->getFailedAttemptCount($username, $attempts); if ($failedCount !== 0) { throw new \RuntimeException( 'Failed attempt counter not reset after successful login (count: ' . $failedCount . ')' ); } echo "✅ Failed attempt counter reset after successful login\n"; } /** * Run all brute force protection tests */ public function runAllTests(): array { $results = []; try { $this->testEnforcesRateLimitOnLoginAttempts(); $results['rate_limiting'] = 'PASS'; } catch (\Exception $e) { $results['rate_limiting'] = 'FAIL: ' . $e->getMessage(); } try { $this->testLocksAccountAfterFailedAttempts(); $results['account_lockout'] = 'PASS'; } catch (\Exception $e) { $results['account_lockout'] = 'FAIL: ' . $e->getMessage(); } try { $this->testImplementsProgressiveDelay(); $results['progressive_delay'] = 'PASS'; } catch (\Exception $e) { $results['progressive_delay'] = 'FAIL: ' . $e->getMessage(); } try { $this->testRequiresCaptchaAfterSuspiciousActivity(); $results['captcha_requirement'] = 'PASS'; } catch (\Exception $e) { $results['captcha_requirement'] = 'FAIL: ' . $e->getMessage(); } try { $this->testDetectsDistributedBruteForce(); $results['distributed_attack'] = 'PASS'; } catch (\Exception $e) { $results['distributed_attack'] = 'FAIL: ' . $e->getMessage(); } try { $this->testDetectsPasswordSprayAttack(); $results['password_spray'] = 'PASS'; } catch (\Exception $e) { $results['password_spray'] = 'FAIL: ' . $e->getMessage(); } try { $this->testSuccessfulLoginResetsAttemptCounter(); $results['counter_reset'] = 'PASS'; } catch (\Exception $e) { $results['counter_reset'] = 'FAIL: ' . $e->getMessage(); } return $results; } private function checkRateLimit( string $ipAddress, array $attempts, int $maxAttempts, int $windowSeconds ): bool { $recentAttempts = array_filter($attempts, function ($attempt) use ($ipAddress, $windowSeconds) { return $attempt['ip'] === $ipAddress && (time() - $attempt['timestamp']) <= $windowSeconds && !$attempt['success']; }); return count($recentAttempts) >= $maxAttempts; } private function checkAccountLockout( string $username, array $attempts, int $maxFailedAttempts, int $lockoutDuration ): array { $failedAttempts = array_filter($attempts, function ($attempt) use ($username) { return $attempt['username'] === $username && !$attempt['success']; }); $isLocked = count($failedAttempts) >= $maxFailedAttempts; return [ 'is_locked' => $isLocked, 'failed_attempts' => count($failedAttempts), 'unlock_at' => $isLocked ? time() + $lockoutDuration : null ]; } private function calculateProgressiveDelay(int $attemptNumber): int { // Exponential backoff: 1s, 2s, 4s, 8s, 16s return min(2 ** ($attemptNumber - 1), 16); } private function shouldRequireCaptcha( string $ipAddress, int $failedAttempts, int $captchaThreshold ): bool { return $failedAttempts >= $captchaThreshold; } private function detectDistributedBruteForce( array $attempts, string $username, int $windowSeconds, int $uniqueIpThreshold ): bool { $recentAttempts = array_filter($attempts, function ($attempt) use ($username, $windowSeconds) { return $attempt['username'] === $username && (time() - $attempt['timestamp']) <= $windowSeconds; }); $uniqueIps = array_unique(array_column($recentAttempts, 'ip')); return count($uniqueIps) >= $uniqueIpThreshold; } private function detectPasswordSpray( array $attempts, int $windowSeconds, int $uniqueUsernameThreshold ): bool { $recentAttempts = array_filter($attempts, function ($attempt) use ($windowSeconds) { return (time() - $attempt['timestamp']) <= $windowSeconds; }); // Group by password $passwordGroups = []; foreach ($recentAttempts as $attempt) { $password = $attempt['password']; if (!isset($passwordGroups[$password])) { $passwordGroups[$password] = []; } $passwordGroups[$password][] = $attempt['username']; } // Check if any password was tried against multiple usernames foreach ($passwordGroups as $password => $usernames) { $uniqueUsernames = array_unique($usernames); if (count($uniqueUsernames) >= $uniqueUsernameThreshold) { return true; } } return false; } private function getFailedAttemptCount(string $username, array $attempts): int { // Find last successful login $lastSuccessIndex = null; foreach ($attempts as $index => $attempt) { if ($attempt['username'] === $username && $attempt['success']) { $lastSuccessIndex = $index; } } // Count failed attempts after last successful login if ($lastSuccessIndex === null) { // No successful login, count all failed attempts return count(array_filter($attempts, fn($a) => $a['username'] === $username && !$a['success'])); } // Count failed attempts after last successful login $failedCount = 0; for ($i = $lastSuccessIndex + 1; $i < count($attempts); $i++) { if ($attempts[$i]['username'] === $username && !$attempts[$i]['success']) { $failedCount++; } } return $failedCount; } }