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>
2026-03-22 20:08:02 +01:00
|
|
|
<?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();
|
|
|
|
|
|
2026-04-01 14:07:49 +02:00
|
|
|
$request = Request::create('/webhooks/stripe/insta', 'POST');
|
|
|
|
|
$request->attributes->set('_route', 'app_stripe_webhook_insta');
|
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>
2026-03-22 20:08:02 +01:00
|
|
|
$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());
|
|
|
|
|
}
|
|
|
|
|
}
|