diff --git a/packages/http/src/Session/VerifyCsrfMiddleware.php b/packages/http/src/Session/VerifyCsrfMiddleware.php index 0e00063f2..be9aa1a3a 100644 --- a/packages/http/src/Session/VerifyCsrfMiddleware.php +++ b/packages/http/src/Session/VerifyCsrfMiddleware.php @@ -7,6 +7,8 @@ use Tempest\Clock\Clock; use Tempest\Core\AppConfig; use Tempest\Core\Priority; +use Tempest\Cryptography\Encryption\Encrypter; +use Tempest\Cryptography\Encryption\Exceptions\EncryptionException; use Tempest\Http\Cookie\Cookie; use Tempest\Http\Cookie\CookieManager; use Tempest\Http\Method; @@ -15,6 +17,7 @@ use Tempest\Http\Session\Session; use Tempest\Router\HttpMiddleware; use Tempest\Router\HttpMiddlewareCallable; +use Tempest\Support\Json\Exception\JsonCouldNotBeDecoded; use Tempest\Support\Str; #[Priority(Priority::FRAMEWORK)] @@ -29,6 +32,7 @@ public function __construct( private SessionConfig $sessionConfig, private CookieManager $cookies, private Clock $clock, + private Encrypter $encrypter, ) {} public function __invoke(Request $request, HttpMiddlewareCallable $next): Response @@ -67,9 +71,18 @@ private function ensureTokenMatches(Request $request): void { $tokenFromRequest = $request->get( key: Session::CSRF_TOKEN_KEY, - default: $request->headers->get(self::CSRF_HEADER_KEY), ); + if (! $tokenFromRequest && $request->headers->has(self::CSRF_HEADER_KEY)) { + try { + $tokenFromRequest = $this->encrypter->decrypt( + urldecode($request->headers->get(self::CSRF_HEADER_KEY)), + ); + } catch (EncryptionException|JsonCouldNotBeDecoded) { + throw new CsrfTokenDidNotMatch(); + } + } + if (! $tokenFromRequest) { throw new CsrfTokenDidNotMatch(); } diff --git a/tests/Integration/Http/CsrfTest.php b/tests/Integration/Http/CsrfTest.php index 0980840cd..3c6d2ff92 100644 --- a/tests/Integration/Http/CsrfTest.php +++ b/tests/Integration/Http/CsrfTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\TestWith; use Tempest\Core\AppConfig; use Tempest\Core\Environment; +use Tempest\Cryptography\Encryption\Encrypter; use Tempest\Http\Cookie\Cookie; use Tempest\Http\GenericRequest; use Tempest\Http\Method; @@ -84,17 +85,35 @@ public function test_matches_from_body(): void ->assertOk(); } - public function test_matches_from_header(): void + public function test_matches_from_header_when_encrypted(): void { $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; - $session = $this->container->get(Session::class); + // Encrypt the token as it would be in a real request + $sessionCookieValue = $this->container + ->get(Encrypter::class) + ->encrypt($session->token) + ->serialize(); + $this->http - ->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $session->token]) + ->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $sessionCookieValue]) ->assertOk(); } + public function test_throws_csrf_exception_when_header_is_non_serialized_hash(): void + { + $this->expectException(CsrfTokenDidNotMatch::class); + $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $session = $this->container->get(Session::class); + + // simulate a non-serialized hash + $sessionCookieValue = 'i-am-not-correct'; + + $this->http + ->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $sessionCookieValue]); + } + public function test_csrf_component(): void { $session = $this->container->get(Session::class);