0
\$\begingroup\$

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.

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:

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
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.

toolic
16.9k6 gold badges30 silver badges224 bronze badges
asked Dec 13 at 18:38
New contributor
rocambille is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.