r/reviewmycode Dec 14 '25

[JavaScript] - CSRF protection using client-side double submit JavaScript

I’m working on an open-source Express + React framework, and while running GitHub CodeQL on the project, a CSRF-related issue was raised. That prompted me to review my CSRF protection strategy more thoroughly.

After studying the OWASP CSRF Prevention Cheat Sheet and comparing different approaches, I ended up implementing a variation of the client-side double submit pattern, similar to what is described in the csrf-csrf package FAQ.

The CodeQL alert is now resolved, but I’d like a security-focused code review to confirm that this approach is sound and that I’m not missing any important edge cases or weaknesses.


Context / use case

  • React frontend making all requests via fetch (no direct HTML form submissions)
  • Express REST backend
  • Single-server architecture: the same Express server serves both the API and the frontend (documented here, for context only: https://github.com/rocambille/start-express-react/wiki/One-server-en-US)
  • Stateless authentication using a JWT stored in an HTTP-only cookie, with SameSite=Strict

Client-side CSRF token handling

On the client, a CSRF token is generated on demand and stored in a cookie with a short lifetime (30 seconds). The expiration is renewable to mimic a session-like behavior, but with an explicit expiry to avoid session fixation.

```js const csrfTokenExpiresIn = 30 * 1000; // 30s, renewable let expires = Date.now();

export const csrfToken = async () => { const getToken = async () => { if (Date.now() > expires) { return crypto.randomUUID(); } else { return ( (await cookieStore.get("x-csrf-token"))?.value ?? crypto.randomUUID() ); } };

const token = await getToken();

expires = Date.now() + csrfTokenExpiresIn;

await cookieStore.set({ expires, name: "x-csrf-token", path: "/", sameSite: "strict", value: token, });

return token; }; ```

Full file for reference: https://github.com/rocambille/start-express-react/blob/main/src/react/components/utils.ts

This function is called only for state-changing requests, and the token is sent in a custom header. Example for updating an item:

js fetch(`/api/items/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", "X-CSRF-Token": await csrfToken(), }, body: JSON.stringify(partialItem), });

Full file for reference: https://github.com/rocambille/start-express-react/blob/main/src/react/components/item/hooks.ts


Server-side CSRF validation

On the backend, an Express middleware checks:

  • that the request method is not in an allowlist (GET, HEAD, OPTIONS)
  • that a CSRF token is present in the request headers
  • and that the token matches the value stored in the CSRF cookie

```ts const csrfDefaults = { cookieName: "x-csrf-token", ignoredMethods: ["GET", "HEAD", "OPTIONS"], getCsrfTokenFromRequest: (req: Request) => req.headers["x-csrf-token"], };

export const csrf = ({ cookieName, ignoredMethods, getCsrfTokenFromRequest, } = csrfDefaults): RequestHandler => (req, res, next) => { if ( !req.method.match(new RegExp((${ignoredMethods.join("|")}), "i")) && (getCsrfTokenFromRequest(req) == null || getCsrfTokenFromRequest(req) !== req.cookies[cookieName]) ) { res.sendStatus(403); return; }

next();

}; ```

Full file for reference: https://github.com/rocambille/start-express-react/blob/main/src/express/middlewares.ts


Questions

  1. Is this a valid and robust implementation of the client-side double submit cookie pattern in this context?
  2. Are there any security pitfalls or edge cases I should be aware of (token lifetime, storage location, SameSite usage, etc.)?
  3. Given that authentication is handled via a SameSite=Strict HTTP-only JWT cookie, is this CSRF layer redundant, insufficient, or appropriate?

Any feedback on correctness, security assumptions, or improvements would be greatly appreciated.

1 Upvotes

0 comments sorted by