Add automatic CSRF protection on all POST forms
- CsrfProtectionSubscriber: auto-injects hidden _csrf_token in HTML responses, auto-verifies on POST requests - Excludes: webhooks, JSON APIs, login (has its own CSRF) - 9 tests covering all cases (GET, excluded, JSON, no token, invalid, valid, inject, non-HTML) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
159
tests/EventSubscriber/CsrfProtectionSubscriberTest.php
Normal file
159
tests/EventSubscriber/CsrfProtectionSubscriberTest.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\EventSubscriber\CsrfProtectionSubscriber;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
class CsrfProtectionSubscriberTest extends TestCase
|
||||
{
|
||||
private function createSubscriber(?CsrfTokenManagerInterface $csrfManager = null): CsrfProtectionSubscriber
|
||||
{
|
||||
$csrfManager ??= $this->createMock(CsrfTokenManagerInterface::class);
|
||||
|
||||
return new CsrfProtectionSubscriber($csrfManager);
|
||||
}
|
||||
|
||||
public function testSubscribedEvents(): void
|
||||
{
|
||||
$events = CsrfProtectionSubscriber::getSubscribedEvents();
|
||||
|
||||
self::assertArrayHasKey(KernelEvents::REQUEST, $events);
|
||||
self::assertArrayHasKey(KernelEvents::RESPONSE, $events);
|
||||
}
|
||||
|
||||
public function testIgnoresGetRequests(): void
|
||||
{
|
||||
$subscriber = $this->createSubscriber();
|
||||
|
||||
$request = Request::create('/test', 'GET');
|
||||
$request->attributes->set('_route', 'app_test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testIgnoresExcludedRoutes(): void
|
||||
{
|
||||
$subscriber = $this->createSubscriber();
|
||||
|
||||
$request = Request::create('/stripe/webhook', 'POST');
|
||||
$request->attributes->set('_route', 'app_stripe_webhook');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testIgnoresJsonRequests(): void
|
||||
{
|
||||
$subscriber = $this->createSubscriber();
|
||||
|
||||
$request = Request::create('/test', 'POST', [], [], [], ['CONTENT_TYPE' => 'application/json']);
|
||||
$request->attributes->set('_route', 'app_test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testIgnoresPostWithoutToken(): void
|
||||
{
|
||||
$subscriber = $this->createSubscriber();
|
||||
|
||||
$request = Request::create('/test', 'POST');
|
||||
$request->attributes->set('_route', 'app_test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testRejectsInvalidToken(): void
|
||||
{
|
||||
$csrfManager = $this->createMock(CsrfTokenManagerInterface::class);
|
||||
$csrfManager->method('isTokenValid')->willReturn(false);
|
||||
|
||||
$subscriber = $this->createSubscriber($csrfManager);
|
||||
|
||||
$request = Request::create('/test', 'POST', ['_csrf_token' => 'bad']);
|
||||
$request->attributes->set('_route', 'app_test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNotNull($event->getResponse());
|
||||
self::assertSame(403, $event->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function testAcceptsValidToken(): void
|
||||
{
|
||||
$csrfManager = $this->createMock(CsrfTokenManagerInterface::class);
|
||||
$csrfManager->method('isTokenValid')->willReturn(true);
|
||||
|
||||
$subscriber = $this->createSubscriber($csrfManager);
|
||||
|
||||
$request = Request::create('/test', 'POST', ['_csrf_token' => 'valid']);
|
||||
$request->attributes->set('_route', 'app_test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
|
||||
|
||||
$subscriber->onKernelRequest($event);
|
||||
|
||||
self::assertNull($event->getResponse());
|
||||
}
|
||||
|
||||
public function testInjectsTokenInHtmlResponse(): void
|
||||
{
|
||||
$csrfManager = $this->createMock(CsrfTokenManagerInterface::class);
|
||||
$csrfManager->method('getToken')->willReturn(new CsrfToken('form_protection', 'abc123'));
|
||||
|
||||
$subscriber = $this->createSubscriber($csrfManager);
|
||||
|
||||
$response = new Response('<form method="post"><button>Submit</button></form>');
|
||||
$response->headers->set('Content-Type', 'text/html');
|
||||
|
||||
$request = Request::create('/test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$subscriber->onKernelResponse($event);
|
||||
|
||||
self::assertStringContainsString('name="_csrf_token"', $response->getContent());
|
||||
self::assertStringContainsString('value="abc123"', $response->getContent());
|
||||
}
|
||||
|
||||
public function testDoesNotInjectInNonHtml(): void
|
||||
{
|
||||
$subscriber = $this->createSubscriber();
|
||||
|
||||
$response = new Response('{"ok":true}');
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
$request = Request::create('/test');
|
||||
$kernel = $this->createMock(HttpKernelInterface::class);
|
||||
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
|
||||
|
||||
$subscriber->onKernelResponse($event);
|
||||
|
||||
self::assertStringNotContainsString('_csrf_token', $response->getContent());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user