|
|
@@ -38,6 +38,10 @@ class SessionManager
|
|
|
private const KEY_FLASH = '_flash';
|
|
|
private const KEY_NEXT = '_next';
|
|
|
private const KEY_OIDC = '_oidc';
|
|
|
+ /** Mirrors `App\Http\CsrfMiddleware::SESSION_KEY` — kept duplicated to
|
|
|
+ * avoid a domain → http-layer dependency on what is otherwise a
|
|
|
+ * value-only collaboration. */
|
|
|
+ private const KEY_CSRF = '_csrf';
|
|
|
|
|
|
private readonly bool $cliFallback;
|
|
|
|
|
|
@@ -90,6 +94,7 @@ class SessionManager
|
|
|
}
|
|
|
if (!($this->headersSentFn)()) {
|
|
|
session_regenerate_id(true);
|
|
|
+ $this->rotateCsrfToken();
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
@@ -99,6 +104,24 @@ class SessionManager
|
|
|
);
|
|
|
}
|
|
|
$this->rotateIdUnderCli();
|
|
|
+ $this->rotateCsrfToken();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SEC_REVIEW F40: drop the cached CSRF token whenever the session id
|
|
|
+ * rotates so a token leaked over the pre-auth privilege boundary
|
|
|
+ * (e.g. via Referer on a public route, a sub-resource integrity
|
|
|
+ * check, or shared with a less-trusted page) cannot be replayed
|
|
|
+ * post-auth. `CsrfMiddleware` lazily mints a fresh token on the
|
|
|
+ * next request when the slot is missing, so the redirect that
|
|
|
+ * always follows `regenerateId()` (303 to /app/dashboard, /no-access,
|
|
|
+ * /login, etc.) re-issues a clean token on its first protected
|
|
|
+ * GET. `clear()` already wipes `$_SESSION` outright on logout, so
|
|
|
+ * the rotate-on-id-rotate hook covers the login direction.
|
|
|
+ */
|
|
|
+ private function rotateCsrfToken(): void
|
|
|
+ {
|
|
|
+ unset($_SESSION[self::KEY_CSRF]);
|
|
|
}
|
|
|
|
|
|
public function setUser(UserContext $user): void
|