CSRF Protection in Single Page Apps: Combining Tokens, SameSite and Fetch
AI generated
JS
() =>
JavaScript · Web Security · Single Page Apps
CSRF Protection in Single Page Apps
combining tokens, SameSite and fetch correctly

Cross Site Request Forgery is considered a solved problem by many teams the moment an application moves to a single page architecture with fetch instead of classic forms. In reality the risk merely shifts as long as session cookies are involved in authenticated API calls, and it stays real, requiring a deliberate combination of double submit cookie, the SameSite attribute and custom headers.

17 min read SameSite · double submit cookie · custom header React · Vue · Vanilla Fetch

1. Why CSRF is underestimated in single page apps

CSRF protection is frequently and incorrectly considered a solved problem in single page app projects once classic HTML forms no longer exist. The misunderstanding arises because many teams equate cross site request forgery with the classic form attack, where a foreign page automatically submits a hidden form. But as long as the application still uses cookie based sessions for authenticated fetch calls, the same underlying mechanism remains attackable: the browser automatically sends cookies along, regardless of whether the request originated from the app's own page or a foreign domain.

The difference from a classic form based application lies in the detail: an attacker can no longer use a simple HTML form submit, but a plain fetch call with credentials: 'include' from a foreign, malicious page achieves the same effect, unless additional CSRF protection kicks in. If the application instead relies exclusively on token based authentication without cookies, for example a bearer token held in JavaScript memory, the classic CSRF risk actually disappears, though other risks such as XSS based token theft appear instead. For cookie based sessions, CSRF protection therefore remains indispensable.

The situation becomes especially deceptive when an application runs several authentication mechanisms in parallel, for example token based authentication for the main API and cookie based sessions for a separate admin area. In that case, a single forgotten endpoint without CSRF protection is enough to undermine the security of the rest of the system.

2. SameSite cookies as the first line of defense

The SameSite attribute of a cookie is the simplest and most effective first line of defense against cross site request forgery. With SameSite=Strict, the browser only sends the cookie on requests originating from the same site, a request initiated from a foreign domain does not even include the session cookie. SameSite=Lax, the default in modern browsers, allows the cookie on simple navigation requests like links, but blocks it on POST requests from foreign sites, which already provides adequate protection for most API calls.

The decisive caveat: SameSite alone is not complete CSRF protection, because older browsers may ignore the attribute, and because subdomain configurations in complex setups can open gaps, for example when a compromised subdomain is considered same site. SameSite=Strict should be the default for session cookies, complemented by a second, independent protection pattern such as the double submit cookie pattern for requests that actually trigger state changes.


Set-Cookie: session_id=abc123; SameSite=Strict; Secure; HttpOnly; Path=/

3. The double submit cookie pattern in detail

The double submit cookie pattern rests on a simple idea: the server sets a random CSRF token both as a readable cookie and additionally expects it as the value of a custom header or request body. An attacker can make the browser send the cookie automatically, but has no access to its value in order to duplicate it in the header, because the same origin policy prevents reading foreign cookies via JavaScript.

Important for single page apps: the CSRF token cookie must not be HttpOnly, since JavaScript needs to explicitly read it and copy it into the header. The actual session cookie, on the other hand, should always stay HttpOnly to protect it from XSS based theft. This separation between a JavaScript readable CSRF token cookie and a hidden, HttpOnly session cookie is the core of robust CSRF protection in modern single page apps.


// Read the CSRF token from a non-HttpOnly cookie and attach it as a header
function getCsrfToken() {
  const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/);
  return match ? decodeURIComponent(match[1]) : null;
}

async function apiPost(url, body) {
  const response = await fetch(url, {
    method: 'POST',
    credentials: 'include', // send the HttpOnly session cookie
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': getCsrfToken(), // server compares this to the session's stored token
    },
    body: JSON.stringify(body),
  });
  if (!response.ok) throw new Error(`Request failed: ${response.status}`);
  return response.json();
}

4. Custom headers as implicit CSRF protection

Beyond the actual token comparison, a second, independent layer of protection exists that emerges almost incidentally from the existing API architecture of a single page app and is frequently overlooked in practice.

An often overlooked but effective piece of CSRF protection comes simply from using a custom header like X-Requested-With or X-CSRF-Token, regardless of the actual token value. Classic HTML forms cannot set custom headers, so a simple cross site form attack already fails because the expected header is simply missing. Fetch calls from a foreign origin also trigger a CORS preflight request for custom headers, which the server can reject if the requesting origin is not on the allowed list.

This implicit protection is valuable but no substitute for an explicit token comparison, because a misconfigured CORS header, for example an overly permissive Access-Control-Allow-Origin: * combined with allowed credentials, can undo the protective effect. Servers should treat CORS configuration and CSRF token checking as two independent controls, not as interchangeable alternatives.

5. Token handling in fetch and Axios interceptors

Clean token handling ultimately comes down to code organization rather than cryptography, which is exactly why it is worth looking at proven patterns from large production codebases.

In larger single page apps with many API calls, it pays off to have a central place for token handling instead of manually setting the CSRF token in every single fetch call. An Axios interceptor or a wrapper around the native fetch API reads the token cookie once and automatically attaches the header to every outgoing request, preventing copy errors and forgotten headers in individual code paths.

In server rendered frameworks like Next.js or Nuxt that support both classic forms and fetch based API routes, the interceptor must also distinguish between an initially server rendered token and a client side refreshed token. A token reissued after login must replace the value held in memory inside the interceptor, otherwise subsequent requests fail with a stale token.


// Centralized Axios interceptor for CSRF token handling
import axios from 'axios';

const api = axios.create({ baseURL: '/api', withCredentials: true });

api.interceptors.request.use((config) => {
  const token = getCsrfToken();
  if (token && ['post', 'put', 'patch', 'delete'].includes(config.method)) {
    config.headers['X-CSRF-Token'] = token;
  }
  return config;
});

export default api;

For React and Vue applications with several independent API clients, it also pays off to have a shared utility function imported by every client, instead of duplicating the token reading code separately in each client. That reduces the chance that a new client is accidentally set up without a CSRF header.

6. Token rotation and expiry

A static CSRF token that stays unchanged for the entire session lifetime is fundamentally secure, but reduces resilience against certain advanced attack scenarios such as session fixation. Best practice is to regenerate the CSRF token on every login and optionally rotate it after a defined time span or after sensitive actions such as password changes.

Important during rotation: the server must still accept the old token for a brief transition period if multiple browser tabs or requests are in flight concurrently, otherwise legitimate requests sent at the same time will fail. A common approach is to include the new token in the response of any API call and adopt it automatically client side, without requiring the user to reload the page.

7. Cross origin APIs and third party integrations

Separate domains for frontend and backend are the norm in modern architectures, which is why every team should engage early with the particulars of cross origin communication, before the first state changing endpoint goes live.

Single page apps that use a separate API domain, for example app.mironsoft.de for the frontend and api.mironsoft.de for the backend, face an additional challenge: SameSite=Strict cookies do not always reliably work as same site between different subdomains, depending on browser interpretation. In that case, SameSite=Lax combined with a strict double submit cookie pattern is often the more practical choice, because it still blocks genuine cross site attacks without breaking legitimate subdomain communication.

For third party integrations, for example an embedded payment widget that itself triggers API calls to the main application, CSRF protection must be explicitly tested for this case. An iframe embedded widget may, depending on context, have no access to the main application's CSRF token cookie, leading to unexpected 403 errors if this case was not accounted for in the token distribution.

In Magento and Hyvä based shops with custom Alpine.js components wired up via fetch, the CSRF token should always be provided centrally through a single shared configuration point, instead of reading it from the cookie separately in every component. That significantly simplifies a later migration to a different token format.

8. Common mistakes in CSRF implementations

The most common mistake is assuming that credentials: 'omit' or entirely leaving out cookies automatically makes an application CSRF safe, while it still uses cookie based sessions elsewhere, for example for a separate admin area. A second mistake is storing the CSRF token in localStorage instead of a dedicated, non HttpOnly cookie, which looks functionally similar but opens additional attack surface for token theft via localStorage access in case of an XSS vulnerability.

A third mistake concerns GET requests: some teams only protect POST, PUT and DELETE requests, forgetting that a GET request with side effects, for instance a logout endpoint implemented via GET, must also be made forgery resistant. The REST convention of never letting GET requests trigger state changes is therefore not just an API design principle, it is a direct CSRF protection measure.

9. CSRF protections compared

Having covered each building block individually, it is worth a summarizing look that places the security level, effort and limits of every measure side by side, so the right combination can be chosen deliberately for a given project.

The following table compares the most important protections that should be combined in single page apps.

Measure Protective effect Implementation effort Limits
SameSite cookie Blocks most cross site requests Very low, one attribute Older browsers, complex subdomains
Double submit cookie Explicitly detects a missing header Client and server changes needed Token cookie must not be HttpOnly
Custom header Blocks classic form attacks Low Depends on correct CORS configuration
Token rotation Reduces the window after a token leak Medium, transition logic needed Race conditions across multiple tabs

In practice, none of these measures alone is robust enough. Combining SameSite=Strict or Lax, a double submit cookie pattern via a custom header, and regular token rotation produces layered CSRF protection that remains effective even if a single layer partially fails.

A recurring penetration test that specifically tries to hit state changing endpoints without a valid CSRF token is the most reliable way to verify the actual effectiveness of the implemented protection layers, rather than relying solely on the theoretical correctness of the configuration.

Mironsoft

Single page app security, CSRF audits and API hardening

Set up CSRF protection for your single page app correctly?

We review your existing fetch and Axios integration, design a fitting double submit cookie pattern, and set up token rotation for critical endpoints without breaking existing API contracts.

CSRF audit

Analysis of all state changing endpoints and cookie configuration

Token integration

Centralized token handling in fetch wrappers and Axios interceptors

Cross origin setup

SameSite and CORS configuration for separate API domains

10. Summary

To close, it is worth revisiting this article's central insight: fetch instead of forms changes the attack vector, but not the need for a well thought out defense concept against cross site request forgery.

CSRF protection in single page apps is not a solved problem just because classic HTML forms have been replaced by fetch calls. As long as cookie based sessions are involved, the underlying mechanism of automatic cookie transmission remains attackable, requiring a deliberate, layered defense. SameSite=Strict or Lax as the first layer, a double submit cookie pattern with a custom header as a second, independent layer, and regular token rotation as a third layer together form a robust protection.

Centralized token handling through fetch wrappers or Axios interceptors substantially reduces the risk of forgotten headers in individual code paths. Anyone who integrates CSRF protection into the application's central API layer from the start, instead of retrofitting it into every single call afterward, saves considerable rework and closes the gap systematically rather than piecemeal.

CSRF Protection in Single Page Apps — The Essentials at a Glance

Four building blocks that together form layered CSRF protection for fetch based applications.

SameSite

SameSite=Strict or Lax blocks most cross site requests, but is no standalone protection.

Double submit cookie

A JavaScript readable token cookie plus a custom header, compared against the session cookie server side.

Centralized handling

An Axios interceptor or fetch wrapper attaches the header automatically to every request.

Token rotation

A new token on login and sensitive actions, with a brief transition window for concurrent requests.

Together these four building blocks form a layered defense that stays effective even if one layer partially fails.

11. FAQ: CSRF Protection in Single Page Apps

The following ten questions summarize the most common practical uncertainties around CSRF protection in single page apps.

1Do I need CSRF protection without HTML forms?
Yes, as long as cookie based sessions are used, the browser sends cookies automatically.
2Is SameSite=Strict enough alone?
No, older browsers and subdomain setups can open gaps. Combine it with double submit cookie.
3What is double submit cookie?
A JavaScript readable token cookie plus a custom header, compared server side against the session value.
4Can the token cookie be HttpOnly?
No, it must be readable by JavaScript. Only the session cookie stays HttpOnly.
5Why does a custom header protect?
Classic forms cannot set custom headers, a missing header exposes the attack.
6How often to rotate?
At least on every login, with a brief transition window for the old token.
7CSRF with a separate API domain?
SameSite=Lax with a strict double submit cookie is more practical than Strict.
8Are GET requests affected?
GET should never trigger state changes; if it does, it needs the same protection.
9Is localStorage a good alternative?
No, it opens extra attack surface under XSS. A dedicated cookie is safer.
10What happens with embedded widgets?
Missing token access in the iframe leads to unexpected 403 errors if unaccounted for.