Skip to main content

CSRF Tokens Explained

CSRF (Cross-Site Request Forgery) is one of the oldest web vulnerabilities. It is also one of the most misunderstood. This page walks slowly through what it actually is, why it works, and the defence Dashify uses.

The setup

You are signed into your bank in one browser tab. You open a different tab and visit a sketchy website. The sketchy website contains an invisible form, automatically submitted, that posts to your bank: "transfer £1000 to account X."

Because cookies are attached to every request the browser makes to your bank, regardless of which tab or website triggered the request, the bank sees a perfectly authenticated request from you, and processes the transfer.

You did not know it happened. You did not click anything. The sketchy website made your browser do it on your behalf.

That is CSRF. The attack relies on the browser's quirk that it attaches cookies to requests automatically, without checking what page made the request.

Why it works

The browser is trying to be helpful. When you go from one tab to another, your bank's cookie should not disappear; you want to stay logged in. The browser attaches the cookie on every request to the bank's domain.

But that attachment is unconditional. It happens whether you visited the bank yourself or whether some other website's JavaScript triggered the request.

CSRF exploits the gap between "this request has the user's cookie" and "this request was actually intended by the user."

The defence, double submit pattern

The fix is to require something the attacker cannot easily get: a token that lives outside the cookie-only mechanism the browser auto-attaches.

The pattern is called double submit:

  1. When the user loads any page, the API issues a fresh CSRF token, a random string. The token goes into the page in two places: a cookie and a piece of JavaScript-readable state (a meta tag or a header).
  2. When the user submits a form (or any state-changing request), the form sends the token both in the cookie and in a custom header.
  3. The API, on receiving the request, compares the cookie token to the header token. If they match, this is a real form submission. If they don't, it is forgery, reject.

The attacker's sketchy website, running on a different domain, can trigger a request to the bank that includes the bank's cookie (cookies are auto attached). It cannot read the bank's cookie value (Same Origin Policy forbids it), and so it cannot put the same value in a custom header. The request arrives with a cookie token but no matching header token, and the bank rejects it.

That is it. The whole CSRF defence in one diagram.

Why a custom header works

Two browser rules combine to make this safe:

  1. Cookies are sent automatically on cross site requests (with SameSite=Lax, only for top-level GETs, but for POSTs they are still attached in some cases).
  2. Custom headers cannot be set on cross site requests without the target site's CORS approval (a preflight happens, and the target site can refuse).

So an attacker can attach the cookie but cannot attach the matching header. The header is the credential that proves "this came from your own page."

Dashify's specifics

Dashify uses the csrf-csrf library, which implements the double submit pattern with a couple of refinements:

  • The token is signed, it contains an HMAC of the random value plus a server side secret. An attacker cannot forge a valid token even if they could read the cookie, because they don't know the secret.
  • The token is session-bound, each session gets its own token, so a token from one user's session cannot be replayed against another's.
  • The cookie holding the CSRF token has its own configuration: HttpOnly: false (because JavaScript needs to read it to put in the header) but Secure: true and SameSite=Strict.

When the page loads, the API issues a fresh CSRF token via GET /auth/csrf. The browser stores it. Every state-changing request (POST, PUT, PATCH, DELETE) sends the token in the X-CSRF-Token header.

What is exempt

Not every endpoint needs CSRF protection.

  • GET requests are exempt because they are supposed to be safe, they should not change state. (If they do, that is a separate bug.)
  • PAT-authenticated requests are exempt because they do not use cookies, there is no automatic-attachment vector to forge.
  • SSO callback URLs are exempt because they receive their security from the OIDC state-and-nonce flow.
  • SCIM endpoints are exempt for the same reason as PATs (server-to-server, no browser involved).

Everything else, gated.

The "SameSite=Lax" overlap

You might wonder: doesn't SameSite=Lax already block CSRF? Mostly, yes. Lax prevents the browser from sending the cookie on cross site form submissions. So a basic CSRF attack via a hidden form is already blocked at the cookie layer.

But Lax does not block every cross site request, top-level GETs still send the cookie, some legitimate third-party flows still need the cookie attached, and older browsers may have edge cases. The double submit token is defence in depth, even if the cookie layer slips, the token layer holds.

In Dashify both layers are active. An attacker would have to defeat both to land a forgery, which they essentially cannot.

What happens when the token expires

The CSRF token is good for the lifetime of the session by default. If a user holds a page open across a session expiry, their next form submission may fail with a CSRF error. The browser silently refetches a new token and retries, the user does not see the failure.

If, for some reason, the retry also fails, the user is sent back to the login page. This is rare but the platform handles it cleanly.

Why this matters more than it seems

CSRF attacks have caused real-world damage to real-world platforms. They are quiet, no XSS pop-up, no obvious sign, and they exploit normal browser behaviour. Defending against them is not optional. The double submit token pattern is the current right answer; Dashify implements it carefully and supplements it with SameSite=Lax cookies for redundancy.

Key takeaways

  • CSRF tricks the user's browser into making a state-changing request without their knowledge.
  • It works because the browser auto-attaches cookies to every request to a domain.
  • Dashify's defence is the double submit pattern: a token in the cookie and in a custom header, both must match.
  • The cookie's SameSite=Lax flag is a second, redundant defence.
  • GET requests, PAT requests, SSO callbacks, and SCIM endpoints are exempt for principled reasons.