fix: CSRF flow ships the raw token, not the HMAC cookie

This commit is contained in:
Keith Solomon
2026-07-26 11:01:36 -05:00
parent 73254cbf80
commit 2ea8cfd83d
13 changed files with 60 additions and 28 deletions
+22 -1
View File
@@ -44,9 +44,15 @@ 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.
$cookies = $_COOKIE;
$existingCookie = (string) ($cookies['__csrf'] ?? '');
if ($existingCookie === '') {
$existingRawToken = (string) ($cookies['__csrf_token'] ?? '');
if ($existingCookie === '' || $existingRawToken === '') {
[$token, $cookie] = CsrfToken::issue($secret);
setcookie('__csrf', $cookie, [
'expires' => time() + 86400,
@@ -55,8 +61,23 @@ if ($existingCookie === '') {
'httponly' => true,
'samesite' => 'Lax',
]);
// The raw token rides in a readable cookie so server-side templates
// can echo it into the <meta name="csrf-token"> tag without keeping
// the raw token in `index.php` per-request state. HttpOnly off so the
// server can read it via the request cookie jar; security comes from
// the HMAC verification, not from hiding the raw token (which the
// browser would already see in the meta tag anyway).
setcookie('__csrf_token', $token, [
'expires' => time() + 86400,
'path' => '/',
'secure' => isset($_SERVER['HTTPS']),
'httponly' => false,
'samesite' => 'Lax',
]);
$existingCookie = $cookie;
$existingRawToken = $token;
$cookies['__csrf'] = $cookie;
$cookies['__csrf_token'] = $token;
}
// 3. Compute the upload-endpoint user token (derived from the same secret).