config = new SecurityHeaderConfig( hsts: true, hstsMaxAge: 31536000, hstsIncludeSubdomains: true, hstsPreload: true, csp: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'", xFrameOptions: 'DENY', xContentTypeOptions: true, referrerPolicy: 'strict-origin-when-cross-origin', permissionsPolicy: "camera=(), microphone=(), geolocation=()" ); $this->middleware = new SecurityHeaderMiddleware($this->config); $this->stateManager = $this->createMock(RequestStateManager::class); } /** * Test: HSTS Header wird korrekt gesetzt * Verhindert SSL-Stripping Attacks */ public function test_hsts_header_set_correctly(): void { // Arrange $request = $this->createHttpsRequest(); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $this->assertEquals( 'max-age=31536000; includeSubDomains; preload', $headers->getFirst('Strict-Transport-Security') ); } /** * Test: CSP Header verhindert XSS * OWASP A03:2021 – Injection */ public function test_csp_header_prevents_xss(): void { // Arrange $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $cspHeader = $headers->getFirst('Content-Security-Policy'); $this->assertStringContains("default-src 'self'", $cspHeader); $this->assertStringContains("script-src 'self'", $cspHeader); $this->assertStringNotContains("'unsafe-eval'", $cspHeader); } /** * Test: X-Frame-Options verhindert Clickjacking * OWASP A05:2021 – Security Misconfiguration */ public function test_x_frame_options_prevents_clickjacking(): void { // Arrange $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $this->assertEquals('DENY', $headers->getFirst('X-Frame-Options')); } /** * Test: X-Content-Type-Options verhindert MIME-Type Confusion */ public function test_x_content_type_options_prevents_mime_sniffing(): void { // Arrange $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $this->assertEquals('nosniff', $headers->getFirst('X-Content-Type-Options')); } /** * Test: Referrer Policy schützt vor Information Leakage */ public function test_referrer_policy_prevents_info_leakage(): void { // Arrange $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $this->assertEquals( 'strict-origin-when-cross-origin', $headers->getFirst('Referrer-Policy') ); } /** * Test: Permissions Policy beschränkt Browser-Features */ public function test_permissions_policy_restricts_features(): void { // Arrange $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert $headers = $result->response->headers; $permissionsPolicy = $headers->getFirst('Permissions-Policy'); $this->assertStringContains('camera=()', $permissionsPolicy); $this->assertStringContains('microphone=()', $permissionsPolicy); $this->assertStringContains('geolocation=()', $permissionsPolicy); } /** * Test: HSTS wird nur über HTTPS gesetzt * Verhindert Mixed Content Probleme */ public function test_hsts_only_over_https(): void { // Arrange: HTTP Request $request = $this->createHttpRequest(); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert: Kein HSTS Header über HTTP $headers = $result->response->headers; $this->assertNull($headers->getFirst('Strict-Transport-Security')); } /** * Test: Security Headers werden nicht überschrieben * Respektiert explizit gesetzte Headers */ public function test_existing_security_headers_not_overridden(): void { // Arrange: Response mit bereits gesetztem CSP Header $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $existingHeaders = ['Content-Security-Policy' => ["default-src 'none'"]]; $response = new HttpResponse(Status::OK, $existingHeaders, 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $this->middleware->__invoke($context, $next, $this->stateManager); // Assert: Ursprünglicher CSP Header bleibt erhalten $headers = $result->response->headers; $this->assertEquals("default-src 'none'", $headers->getFirst('Content-Security-Policy')); } /** * Test: Development-spezifische CSP Policy * Erlaubt eval() und inline styles in Development */ public function test_development_csp_policy(): void { // Arrange: Development Config $devConfig = new SecurityHeaderConfig( csp: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" ); $middleware = new SecurityHeaderMiddleware($devConfig); $request = $this->createMock(Request::class); $context = new MiddlewareContext($request); $response = new HttpResponse(Status::OK, [], 'content'); $next = $this->createMock(Next::class); $next->method('__invoke')->willReturn(new MiddlewareContext($request, $response)); // Act $result = $middleware->__invoke($context, $next, $this->stateManager); // Assert: Development CSP erlaubt unsafe-eval $headers = $result->response->headers; $cspHeader = $headers->getFirst('Content-Security-Policy'); $this->assertStringContains("'unsafe-eval'", $cspHeader); $this->assertStringContains("'unsafe-inline'", $cspHeader); } // Helper Methods private function createHttpsRequest(): Request { $request = $this->createMock(Request::class); $request->method('isSecure')->willReturn(true); return $request; } private function createHttpRequest(): Request { $request = $this->createMock(Request::class); $request->method('isSecure')->willReturn(false); return $request; } }