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
This commit is contained in:
230
tests/Framework/Security/CsrfSecurityTest.php
Normal file
230
tests/Framework/Security/CsrfSecurityTest.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user