csrfTokenGenerator->generate(); if (strlen($token) < 16) { throw new \RuntimeException( "CSRF token too short: " . strlen($token) . " characters (minimum 16 required)" ); } echo "✅ CSRF token generated successfully (length: " . strlen($token) . ")\n"; } /** * Test CSRF tokens are unique */ public function testTokensAreUnique(): void { $tokens = []; for ($i = 0; $i < 100; $i++) { $tokens[] = $this->csrfTokenGenerator->generate(); } $uniqueTokens = array_unique($tokens); if (count($uniqueTokens) !== count($tokens)) { throw new \RuntimeException( "CSRF tokens are not unique: " . count($uniqueTokens) . " unique out of " . count($tokens) ); } echo "✅ All CSRF tokens are unique (tested 100 tokens)\n"; } /** * Test CSRF token validation */ public function testValidatesCorrectToken(): void { $token = $this->csrfTokenGenerator->generate(); // Store token in session simulation $_SESSION['csrf_token'] = $token; // Validate same token if ($token !== $_SESSION['csrf_token']) { throw new \RuntimeException("CSRF token validation failed"); } echo "✅ CSRF token validation works correctly\n"; } /** * Test CSRF token mismatch detection */ public function testDetectsTokenMismatch(): void { $validToken = $this->csrfTokenGenerator->generate(); $invalidToken = $this->csrfTokenGenerator->generate(); $_SESSION['csrf_token'] = $validToken; if ($invalidToken === $_SESSION['csrf_token']) { throw new \RuntimeException("CSRF token mismatch not detected"); } echo "✅ CSRF token mismatch detected correctly\n"; } /** * Test CSRF token missing detection */ public function testDetectsMissingToken(): void { // Clear session token unset($_SESSION['csrf_token']); $providedToken = $this->csrfTokenGenerator->generate(); if (isset($_SESSION['csrf_token']) && $_SESSION['csrf_token'] === $providedToken) { throw new \RuntimeException("Missing CSRF token not detected"); } echo "✅ Missing CSRF token detected correctly\n"; } /** * Test CSRF protection for POST requests */ public function testRequiresCsrfForPostRequests(): void { // POST requests without CSRF token should be rejected $request = $this->createRequest(Method::POST, [], false); if (!$this->shouldRejectRequest($request)) { throw new \RuntimeException("POST request without CSRF token was not rejected"); } echo "✅ POST requests without CSRF token are rejected\n"; } /** * Test CSRF protection allows GET requests */ public function testAllowsGetRequests(): void { // GET requests don't require CSRF token $request = $this->createRequest(Method::GET, [], false); if ($this->shouldRejectRequest($request)) { throw new \RuntimeException("GET request was incorrectly rejected"); } echo "✅ GET requests are allowed without CSRF token\n"; } /** * Test CSRF token rotation */ public function testTokenRotation(): void { $token1 = $this->csrfTokenGenerator->generate(); $_SESSION['csrf_token'] = $token1; // Simulate token rotation after successful request $token2 = $this->csrfTokenGenerator->generate(); $_SESSION['csrf_token'] = $token2; if ($token1 === $token2) { throw new \RuntimeException("CSRF token not rotated"); } if ($_SESSION['csrf_token'] === $token1) { throw new \RuntimeException("Old CSRF token still valid after rotation"); } echo "✅ CSRF token rotation works correctly\n"; } /** * Run all CSRF protection tests */ public function runAllTests(): array { $results = []; try { $this->testGeneratesValidToken(); $results['token_generation'] = 'PASS'; } catch (\Exception $e) { $results['token_generation'] = 'FAIL: ' . $e->getMessage(); } try { $this->testTokensAreUnique(); $results['token_uniqueness'] = 'PASS'; } catch (\Exception $e) { $results['token_uniqueness'] = 'FAIL: ' . $e->getMessage(); } try { $this->testValidatesCorrectToken(); $results['token_validation'] = 'PASS'; } catch (\Exception $e) { $results['token_validation'] = 'FAIL: ' . $e->getMessage(); } try { $this->testDetectsTokenMismatch(); $results['mismatch_detection'] = 'PASS'; } catch (\Exception $e) { $results['mismatch_detection'] = 'FAIL: ' . $e->getMessage(); } try { $this->testDetectsMissingToken(); $results['missing_token_detection'] = 'PASS'; } catch (\Exception $e) { $results['missing_token_detection'] = 'FAIL: ' . $e->getMessage(); } try { $this->testRequiresCsrfForPostRequests(); $results['post_protection'] = 'PASS'; } catch (\Exception $e) { $results['post_protection'] = 'FAIL: ' . $e->getMessage(); } try { $this->testAllowsGetRequests(); $results['get_allowed'] = 'PASS'; } catch (\Exception $e) { $results['get_allowed'] = 'FAIL: ' . $e->getMessage(); } try { $this->testTokenRotation(); $results['token_rotation'] = 'PASS'; } catch (\Exception $e) { $results['token_rotation'] = 'FAIL: ' . $e->getMessage(); } return $results; } private function createRequest(Method $method, array $postData = [], bool $includeCsrf = false): HttpRequest { if ($includeCsrf && $method !== Method::GET) { $postData['_csrf_token'] = $_SESSION['csrf_token'] ?? ''; } $parsedUri = ParsedUri::fromString('https://localhost/api/test'); $server = new ServerEnvironment([ 'REQUEST_METHOD' => $method->value, 'REQUEST_URI' => '/api/test', 'SERVER_NAME' => 'localhost', 'SERVER_PORT' => '443', 'HTTPS' => 'on' ]); return new HttpRequest( method: $method, uri: $parsedUri, server: $server, headers: [], body: !empty($postData) ? json_encode($postData) : '', parsedBody: !empty($postData) ? $postData : null, queryParameters: [], cookies: [], files: [] ); } private function shouldRejectRequest(HttpRequest $request): bool { // POST, PUT, DELETE, PATCH require CSRF token if (in_array($request->method, [Method::POST, Method::PUT, Method::DELETE, Method::PATCH])) { $csrfToken = $request->parsedBody['_csrf_token'] ?? null; $sessionToken = $_SESSION['csrf_token'] ?? null; return $csrfToken !== $sessionToken || $csrfToken === null; } return false; } }