session = $this->createMock(SessionInterface::class); $this->middleware = new CsrfMiddleware($this->session); $this->stateManager = $this->createMock(RequestStateManager::class); } /** * Test: CSRF-Schutz für state-changing Operations * OWASP A01:2021 – Broken Access Control */ public function test_csrf_protection_blocks_post_without_token(): void { // Arrange: Session ist gestartet aber kein CSRF Token $this->session->method('isStarted')->willReturn(true); $request = $this->createPostRequest([], []); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); // Act & Assert: Exception erwartet $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('CSRF protection requires both form ID and token'); $this->middleware->__invoke($context, $next, $this->stateManager); } /** * Test: CSRF-Token Validation gegen Token-Forgery */ public function test_csrf_validation_rejects_invalid_token(): void { // Arrange: Ungültiges Token $this->session->method('isStarted')->willReturn(true); $csrf = $this->createMock(\App\Framework\Http\Session\CsrfProtection::class); $csrf->method('validateToken')->willReturn(false); $this->session->csrf = $csrf; $request = $this->createPostRequest([ '_form_id' => 'login_form', '_token' => 'invalid_token_value', ], []); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); // Act & Assert $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('CSRF token validation failed'); $this->middleware->__invoke($context, $next, $this->stateManager); } /** * Test: Valide CSRF-Token werden akzeptiert */ public function test_csrf_validation_accepts_valid_token(): void { // Arrange: Valides Token $this->session->method('isStarted')->willReturn(true); $csrf = $this->createMock(\App\Framework\Http\Session\CsrfProtection::class); $csrf->method('validateToken') ->with('login_form', $this->isInstanceOf(CsrfToken::class)) ->willReturn(true); $this->session->csrf = $csrf; $request = $this->createPostRequest([ '_form_id' => 'login_form', '_token' => 'valid_token_abc123', ], []); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); $next->expects($this->once())->method('__invoke')->willReturn($context); // Act: Sollte ohne Exception durchlaufen $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $this->assertSame($context, $result); } /** * Test: GET-Requests benötigen keinen CSRF-Schutz */ public function test_get_requests_bypass_csrf_validation(): void { // Arrange: GET Request ohne Token $this->session->method('isStarted')->willReturn(true); $request = $this->createGetRequest(); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); $next->expects($this->once())->method('__invoke')->willReturn($context); // Act: Sollte ohne Validation durchlaufen $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $this->assertSame($context, $result); } /** * Test: Session muss vor CSRF-Validation gestartet sein * Verhindert Race Conditions */ public function test_requires_started_session(): void { // Arrange: Session nicht gestartet $this->session->method('isStarted')->willReturn(false); $request = $this->createPostRequest(['_token' => 'abc'], []); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); // Act & Assert $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Session must be started before CSRF validation'); $this->middleware->__invoke($context, $next, $this->stateManager); } /** * Test: CSRF-Token über HTTP Headers (AJAX) */ public function test_csrf_token_via_headers(): void { // Arrange: Token über Headers statt Form Data $this->session->method('isStarted')->willReturn(true); $csrf = $this->createMock(\App\Framework\Http\Session\CsrfProtection::class); $csrf->method('validateToken')->willReturn(true); $this->session->csrf = $csrf; $request = $this->createPostRequest([], [ 'X-CSRF-Form-ID' => 'ajax_form', 'X-CSRF-Token' => 'header_token_xyz', ]); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); $next->expects($this->once())->method('__invoke')->willReturn($context); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $this->assertSame($context, $result); } /** * Test: Malformed CSRF Token wird abgelehnt */ public function test_malformed_csrf_token_rejected(): void { // Arrange: Malformed Token $this->session->method('isStarted')->willReturn(true); $request = $this->createPostRequest([ '_form_id' => 'form1', '_token' => 'malformed token with spaces and @#$%', ], []); $context = new MiddlewareContext($request); $next = $this->createMock(Next::class); // Act & Assert: Exception bei malformed Token $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid CSRF token format'); $this->middleware->__invoke($context, $next, $this->stateManager); } // Helper Methods private function createPostRequest(array $body, array $headers): Request { $request = $this->createMock(Request::class); $request->method = Method::POST; $parsedBody = $this->createMock(\App\Framework\Http\RequestBody::class); $parsedBody->method('get')->willReturnCallback(fn ($key) => $body[$key] ?? null); $request->parsedBody = $parsedBody; $headersBag = $this->createMock(\App\Framework\Http\Headers::class); $headersBag->method('getFirst')->willReturnCallback(fn ($key) => $headers[$key] ?? null); $request->headers = $headersBag; return $request; } private function createGetRequest(): Request { $request = $this->createMock(Request::class); $request->method = Method::GET; return $request; } }