Files
michaelschiemer/tests/Framework/Security/CsrfSecurityTest.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

231 lines
7.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Tests\Framework\Security;
use App\Framework\Http\Method;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\Middlewares\CsrfMiddleware;
use App\Framework\Http\Next;
use App\Framework\Http\Request;
use App\Framework\Http\RequestStateManager;
use App\Framework\Http\Session\Session;
use App\Framework\Http\Session\SessionInterface;
use App\Framework\Security\CsrfToken;
use PHPUnit\Framework\TestCase;
/**
* Critical Security Tests for CSRF Protection
* Tests gegen OWASP A01:2021 Broken Access Control
*/
final class CsrfSecurityTest extends TestCase
{
private SessionInterface $session;
private CsrfMiddleware $middleware;
private RequestStateManager $stateManager;
protected function setUp(): void
{
$this->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;
}
}