Skip to content

Commit d1ee721

Browse files
fix(http): desrialize csrf token from headers (#1616)
1 parent 538246d commit d1ee721

File tree

2 files changed

+36
-4
lines changed

2 files changed

+36
-4
lines changed

packages/http/src/Session/VerifyCsrfMiddleware.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
use Tempest\Clock\Clock;
88
use Tempest\Core\AppConfig;
99
use Tempest\Core\Priority;
10+
use Tempest\Cryptography\Encryption\Encrypter;
11+
use Tempest\Cryptography\Encryption\Exceptions\EncryptionException;
1012
use Tempest\Http\Cookie\Cookie;
1113
use Tempest\Http\Cookie\CookieManager;
1214
use Tempest\Http\Method;
@@ -15,6 +17,7 @@
1517
use Tempest\Http\Session\Session;
1618
use Tempest\Router\HttpMiddleware;
1719
use Tempest\Router\HttpMiddlewareCallable;
20+
use Tempest\Support\Json\Exception\JsonCouldNotBeDecoded;
1821
use Tempest\Support\Str;
1922

2023
#[Priority(Priority::FRAMEWORK)]
@@ -29,6 +32,7 @@ public function __construct(
2932
private SessionConfig $sessionConfig,
3033
private CookieManager $cookies,
3134
private Clock $clock,
35+
private Encrypter $encrypter,
3236
) {}
3337

3438
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
@@ -67,9 +71,18 @@ private function ensureTokenMatches(Request $request): void
6771
{
6872
$tokenFromRequest = $request->get(
6973
key: Session::CSRF_TOKEN_KEY,
70-
default: $request->headers->get(self::CSRF_HEADER_KEY),
7174
);
7275

76+
if (! $tokenFromRequest && $request->headers->has(self::CSRF_HEADER_KEY)) {
77+
try {
78+
$tokenFromRequest = $this->encrypter->decrypt(
79+
urldecode($request->headers->get(self::CSRF_HEADER_KEY)),
80+
);
81+
} catch (EncryptionException|JsonCouldNotBeDecoded) {
82+
throw new CsrfTokenDidNotMatch();
83+
}
84+
}
85+
7386
if (! $tokenFromRequest) {
7487
throw new CsrfTokenDidNotMatch();
7588
}

tests/Integration/Http/CsrfTest.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPUnit\Framework\Attributes\TestWith;
66
use Tempest\Core\AppConfig;
77
use Tempest\Core\Environment;
8+
use Tempest\Cryptography\Encryption\Encrypter;
89
use Tempest\Http\Cookie\Cookie;
910
use Tempest\Http\GenericRequest;
1011
use Tempest\Http\Method;
@@ -84,17 +85,35 @@ public function test_matches_from_body(): void
8485
->assertOk();
8586
}
8687

87-
public function test_matches_from_header(): void
88+
public function test_matches_from_header_when_encrypted(): void
8889
{
8990
$this->container->get(AppConfig::class)->environment = Environment::PRODUCTION;
90-
9191
$session = $this->container->get(Session::class);
9292

93+
// Encrypt the token as it would be in a real request
94+
$sessionCookieValue = $this->container
95+
->get(Encrypter::class)
96+
->encrypt($session->token)
97+
->serialize();
98+
9399
$this->http
94-
->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $session->token])
100+
->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $sessionCookieValue])
95101
->assertOk();
96102
}
97103

104+
public function test_throws_csrf_exception_when_header_is_non_serialized_hash(): void
105+
{
106+
$this->expectException(CsrfTokenDidNotMatch::class);
107+
$this->container->get(AppConfig::class)->environment = Environment::PRODUCTION;
108+
$session = $this->container->get(Session::class);
109+
110+
// simulate a non-serialized hash
111+
$sessionCookieValue = 'i-am-not-correct';
112+
113+
$this->http
114+
->post('/test', headers: [VerifyCsrfMiddleware::CSRF_HEADER_KEY => $sessionCookieValue]);
115+
}
116+
98117
public function test_csrf_component(): void
99118
{
100119
$session = $this->container->get(Session::class);

0 commit comments

Comments
 (0)