Summary
Add an optional Origin / Sec-Fetch-Site same-origin check to SessionCSRF, as a complement to the existing session-bound token, so a legitimate same-origin form POST still validates when the session cookie isn't present on the request.
Background
SessionCSRF is a session-bound synchronizer token: the token is stored in the session and validate() / hasValidToken() compare the submitted value against it. That's solid, but it has a hard dependency — the session cookie must reach the server on the POST. When it doesn't, the token check fails and (with a friendly retry handler) the user can end up in a loop.
On real mobile/desktop traffic the cookie is absent on the POST more often than you'd expect, for legitimate same-origin submissions:
- Cross-site entry funnels. Users arriving from an external link (email/SMS, an emailed coupon URL), returning from a payment provider (e.g. Paddle), or from an OAuth provider (Google) navigate "cross-site," so the browser withholds a
SameSite=Lax session cookie on the resulting form POST. Modern Chrome evaluates the full redirect chain, so this is browser-agnostic — we reproduced it on desktop Chrome, not just Safari/iOS.
- Session not found for the cookie's id (GC, a fresh session minted under concurrent requests, etc.) — cookie sent, but the token isn't in that session.
In all these cases the token check fails for a legitimate same-origin submission, and there's no clean recovery.
What other frameworks do
A header-based same-origin check is the modern complement and is now mainstream:
- Laravel 13's default CSRF middleware checks
Sec-Fetch-Site first and accepts same-origin requests with no token, falling back to the token otherwise (plus an originOnly mode).
- Django supplements its CSRF token with an
Origin check (and strict Referer on HTTPS when Origin is absent).
- OWASP lists Origin/Referer verification as a primary CSRF mechanism, with
SameSite as defense-in-depth only.
Origin and Sec-Fetch-Site are browser-controlled and cannot be forged cross-site, so this blocks cross-site forgery without depending on the session cookie surviving the round trip.
Request
Add an opt-in (or default) Origin / Sec-Fetch-Site verification to SessionCSRF: when a request is provably same-origin (Origin host ∈ $config->httpHosts, or Sec-Fetch-Site: same-origin), accept it even if the session-bound token check fails. Keep the session token as the primary defense and use this purely as the cookie-independent fallback.
Why this is awkward to add in userland today
SessionCSRF::validate() and hasValidToken() aren't hookable (no ___ prefix), and hasValidToken() doesn't honor $config->protectCSRF. To inject an Origin check from a site/module today you have to either seed the session token or toggle $config->protectCSRF per request — both hacky. Native support would be much cleaner and would fix a real class of mobile login/signup failures.
Happy to sketch a PR if useful. (ProcessWire 3.x / dev.)
Summary
Add an optional
Origin/Sec-Fetch-Sitesame-origin check toSessionCSRF, as a complement to the existing session-bound token, so a legitimate same-origin form POST still validates when the session cookie isn't present on the request.Background
SessionCSRFis a session-bound synchronizer token: the token is stored in the session andvalidate()/hasValidToken()compare the submitted value against it. That's solid, but it has a hard dependency — the session cookie must reach the server on the POST. When it doesn't, the token check fails and (with a friendly retry handler) the user can end up in a loop.On real mobile/desktop traffic the cookie is absent on the POST more often than you'd expect, for legitimate same-origin submissions:
SameSite=Laxsession cookie on the resulting form POST. Modern Chrome evaluates the full redirect chain, so this is browser-agnostic — we reproduced it on desktop Chrome, not just Safari/iOS.In all these cases the token check fails for a legitimate same-origin submission, and there's no clean recovery.
What other frameworks do
A header-based same-origin check is the modern complement and is now mainstream:
Sec-Fetch-Sitefirst and accepts same-origin requests with no token, falling back to the token otherwise (plus anoriginOnlymode).Origincheck (and strictRefereron HTTPS whenOriginis absent).SameSiteas defense-in-depth only.OriginandSec-Fetch-Siteare browser-controlled and cannot be forged cross-site, so this blocks cross-site forgery without depending on the session cookie surviving the round trip.Request
Add an opt-in (or default)
Origin/Sec-Fetch-Siteverification toSessionCSRF: when a request is provably same-origin (Originhost ∈$config->httpHosts, orSec-Fetch-Site: same-origin), accept it even if the session-bound token check fails. Keep the session token as the primary defense and use this purely as the cookie-independent fallback.Why this is awkward to add in userland today
SessionCSRF::validate()andhasValidToken()aren't hookable (no___prefix), andhasValidToken()doesn't honor$config->protectCSRF. To inject an Origin check from a site/module today you have to either seed the session token or toggle$config->protectCSRFper request — both hacky. Native support would be much cleaner and would fix a real class of mobile login/signup failures.Happy to sketch a PR if useful. (ProcessWire 3.x / dev.)