2

I'm building a Next.js application and on one of the pages, I need to call one of the /api routes.

Calling this API route carries out a state-changing operation, so it's important to make sure the call is not coming from an attacker trying to impersonate my user.

Take the following as an example:

fetch('/api/grantPermissions', {
  headers: ...,
  method: 'POST',
  body: JSON.stringify({resource: 'someresourceid', permission: 'somepermission'})
})

I've noticed there aren't many solutions for protecting a Next.js API route from a CSRF attack, so what I was considering is the following:

  1. When the user logs in, a random 32 byte hex string is generated
  2. It is stored in the session object (using iron-session)
  3. Using getServerSideProps(), the string stored in the session is injected into the page that needs to make the fetch call
  4. When the fetch call is being made, the CSRF token is attached with the request (e.g. in the body or custom header)
  5. The /api/grant route then checks if the CSRF token provided is the same as the one in the session

Is this a secure way of preventing a CSRF attack using the Synchronizer Token Pattern? What vulnerabilities could this approach lead to?

Chris Yalamov
  • 80
  • 2
  • 7

2 Answers2

5

I would first consider if setting SameSite=Lax for the session cookie is good enough?

Is this a secure way of preventing a CSRF attack using the Synchronizer Token Pattern?

Four possible points of improvements:

  1. Sessions must be invalidated for rotation of CSRF tokens.
  2. The random 32 byte hex string should be generated with a cryptographically strong pseudo random number generator.
  3. You don't mention CSRF protection for login, which can mitigate session fixation attacks.
  4. It looks like iron-session uses stateless sessions, implying there is no state on the backend. This sounds like the use case for the Double Submit Cookie Pattern. However, it seems iron-session is implemented using an encrypted and signed cookie. Given that you trust the iron-session, I believe the Synchronizer Token Pattern should work if you remember to verify the signature of the session cookie before checking if the CSRF token from the session cookie matches the CSRF token in the custom request header.

The things I like with your approach:

If you consider the four points of improvements, I believe you have a more secure implementation of the Synchronizer Token Pattern. However, I am not the one to answer if it is secure enough given your risk tolerance. Check out the OWASP CSRF cheatsheet if you haven't seen it already.

What vulnerabilities could this approach lead to?

  • CSRF protection can be bypassed if you have a XSS vulnerability.
  • Per-session tokens has the time range for a valid session where an attacker can exploit a stolen CSRF token.
  • You are dependant on iron-session.

Regarding existing solutions for protecting a Next.js API route from CSRF attacks:

Good luck!

Kaffekoppen
  • 392
  • 3
  • 12
2

With a lot of inspiration from: https://levelup.gitconnected.com/how-to-implement-csrf-tokens-in-express-f867c9e95af0

import { randomBytes } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { GetServerSideProps } from 'next';

export default function setCsrfCookie(req:NextRequest, res:NextResponse):string {
  if (typeof window !== 'undefined') {
    return '';
  }
  const token = randomBytes(100).toString('base64');
  // @ts-ignore
  res.setHeader("set-cookie", `csrf_token=${token}; path=/; samesite=Strict; httponly;`);

  return token;
}

export default function Page(props: { csrfToken: string }) {
    return (
      <form method="post">
        <input type="hidden" name="csrf_token" value={csrfToken} />
      </form>
    )
}

export async function getServerSideProps(context:GetServerSideProps) {
  const { locale, req, res } = context;
  const csrfToken = setCsrfCookie(req, res);

  return {
    props: {
      csrfToken,
    },
  }
}

Then in the API route you submit to you can verify that the cookie csrf_token equals the form body value.

OZZIE
  • 6,609
  • 7
  • 55
  • 59
  • I have though about though, the solution I linked to only seems to compare that they are the same, shouldn't there be some check that this token is still valid also? Also that the cookie is a serverside set cookie (httpOnly)? Otherwise can't you spoof this solution with a clientside/js set cookie instead? – OZZIE Jun 16 '23 at 11:41