When a browser session predates a server restart with a fresh secret.key, the old __csrf HMAC cookie is no longer verifiable under the new secret. Every action POST returns 403 until the user manually clears cookies, because the existing-pair check on the front controller was 'is the cookie present?' rather than 'does the cookie verify under the current secret?'. The new check at the top of the front controller computes the expected HMAC of the raw-token cookie under the current secret and constant-time-compares it to the __csrf cookie. If they don't match, the controller re-mints a fresh pair on the response, replacing the browser's stale cookies with a working set. The next click of 'Play bundled' then succeeds without user intervention. Test coverage in tests/Integration/StaleCookieRotationTest.php: - testStaleCookiesAreReplacedOnNextRequest - testValidCookiesAreNotReIssuedOnNextRequest - testMissingTokenCookieIsFilledInOnNextRequest
This commit is contained in:
+19
-7
@@ -43,16 +43,28 @@ if ($secret === false || $secret === '') {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Ensure the __csrf cookie is set. If absent, issue a fresh token.
|
||||
// The double-submit pattern stores the raw token in a separate readable
|
||||
// cookie so client code (meta tag, JS) can echo it back as the
|
||||
// X-CSRF-Token header. The HMAC cookie is the verification oracle;
|
||||
// the raw-token cookie rides parallel and is readable by document.cookie
|
||||
// or the equivalent server-side read.
|
||||
// 2. Ensure the __csrf cookie is set. If absent OR stale, issue a fresh
|
||||
// token. A cookie pair is "stale" when both cookies are present but
|
||||
// the HMAC (__csrf) does not match the raw token (__csrf_token) under
|
||||
// the current secret. This can happen when a browser session predates
|
||||
// a server restart with a fresh secret.key: the browser still has the
|
||||
// old __csrf cookie, the new server's secret does not match it, and
|
||||
// every CSRF check fails. By re-minting on first request, we recover
|
||||
// the session without requiring the user to clear cookies manually.
|
||||
// The double-submit pattern stores the raw token in a separate
|
||||
// readable cookie so client code (meta tag, JS) can echo it back as
|
||||
// the X-CSRF-Token header. The HMAC cookie is the verification
|
||||
// oracle; the raw-token cookie rides parallel and is readable by
|
||||
// document.cookie or the equivalent server-side read.
|
||||
$cookies = $_COOKIE;
|
||||
$existingCookie = (string) ($cookies['__csrf'] ?? '');
|
||||
$existingRawToken = (string) ($cookies['__csrf_token'] ?? '');
|
||||
if ($existingCookie === '' || $existingRawToken === '') {
|
||||
$pairIsFresh = false;
|
||||
if ($existingCookie !== '' && $existingRawToken !== '') {
|
||||
$expectedHmac = hash_hmac('sha256', $existingRawToken, $secret);
|
||||
$pairIsFresh = hash_equals($expectedHmac, $existingCookie);
|
||||
}
|
||||
if ($existingCookie === '' || $existingRawToken === '' || !$pairIsFresh) {
|
||||
[$token, $cookie] = CsrfToken::issue($secret);
|
||||
setcookie('__csrf', $cookie, [
|
||||
'expires' => time() + 86400,
|
||||
|
||||
Reference in New Issue
Block a user