- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
282 lines
9.6 KiB
PHP
282 lines
9.6 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Tests\Framework\Security;
|
||
|
||
use App\Framework\Http\HttpResponse;
|
||
use App\Framework\Http\MiddlewareContext;
|
||
use App\Framework\Http\Middlewares\SecurityHeaderConfig;
|
||
use App\Framework\Http\Middlewares\SecurityHeaderMiddleware;
|
||
use App\Framework\Http\Next;
|
||
use App\Framework\Http\Request;
|
||
use App\Framework\Http\RequestStateManager;
|
||
use App\Framework\Http\Status;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
/**
|
||
* Critical Security Tests for HTTP Security Headers
|
||
* Tests gegen OWASP A05:2021 – Security Misconfiguration
|
||
*/
|
||
final class SecurityHeadersTest extends TestCase
|
||
{
|
||
private SecurityHeaderConfig $config;
|
||
|
||
private SecurityHeaderMiddleware $middleware;
|
||
|
||
private RequestStateManager $stateManager;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->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;
|
||
}
|
||
}
|