kubetail-org / edge-csrf

CSRF protection library for JavaScript that runs on the edge runtime (with Next.js, SvelteKit, Express, Node-HTTP integrations)
MIT License
138 stars 7 forks source link

Question on /csrf-token endpoint #49

Open justin0108 opened 2 months ago

justin0108 commented 2 months ago

Hi, I am quite new to CSRF and was using your library for NextJS 14. Can I check with regards to this portion of code fetch("/csrf-token"):

const handleClick2 = async () => {
    const csrfResp = await fetch("/csrf-token");
    const { csrfToken } = await csrfResp.json();
    ...

and middleware.ts

  // return token (for use in static-optimized-example)
  if (request.nextUrl.pathname === '/csrf-token') {
    return NextResponse.json({ csrfToken: response.headers.get('X-CSRF-Token') || 'missing' });
  }

is this endpoint /csrf-token suppose to be there in production as well? or this is just an example?

I am reading article which indicate CSRF token should not be accessed with AJAX which confuses me with your example. Thanks

amorey commented 2 months ago

Typically the recommended way to share a csrf-token with the client is by it placing it in a <meta> tag in the html response but tokens transmitted in this way can be accessed from client-side javascript as easily as from an explicit /csrf-token endpoint so I don't see what the difference is:

const resp = await fetch('/page-with-csrf-token-in-meta-tag');
const html = await resp.text();
// use regex to parse `<meta name="csrf-token" content="xxx">` here...

One possible attack that they're trying to protect against is that if the /csrf-token endpoint's response can be read from a third-party website (e.g. CORS-enabled) then it can be used in an attack. But this is also true of any page that contains CSRF tokens.

CSRF protection is a deep field so I don't want to say with 100% certainty that you can disregard that comment but I can't see how the specific /csrf-token endpoint in the Next.js example is insecure. Of course, I'd love for a security researcher with more knowledge than me to give us their opinion in the comments.

Incidentally, if you want to move the CSRF token to the <meta> tag in the example you can modify the layout file like this:

// app/layout.tsx

import { Metadata } from 'next';
import { headers } from 'next/headers';

export async function generateMetadata(): Promise<Metadata> {
  const csrfToken = headers().get('X-CSRF-Token') || 'missing';
  return {
    title: 'edge-csrf examples',
    other: {
      'csrf-token': csrfToken,
    },
  };
}

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
      </body>
    </html>
  );
}

And the handleSubmit() method like this:

const handleSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
  // prevent default form submission
  ev.preventDefault();

  // get form values
  const data = new FormData(ev.currentTarget);

  // get token from <meta> tag
  const metaTag = document.querySelector('meta[name="csrf-token"]');
  const csrfToken = metaTag ? metaTag.getAttribute('content') : 'missing';

  // build fetch args
  const fetchArgs = { method: 'POST', headers: {}, body: JSON.stringify(data) };
  if (csrfToken) fetchArgs.headers = { 'X-CSRF-Token': csrfToken };

  // send to backend
  const response = await fetch('/form-handler', fetchArgs);

  // show response
  // eslint-disable-next-line no-alert
  alert(response.statusText);
};
justin0108 commented 2 months ago

Thanks for your detail explanation. The OWASP link is very useful to help me better understand CSRF.

I was looking at their recommendation to get the token "Signed Double-Submit Cookie". It seems that it is "safer" way to get the token via the cookie? I am no expert in the security field either. Any guidance/comments in this area is much appreciated.

With CORS disabled in your example, I don't see how an attacker can gain access to the csrf token either. So to me it seems both method is equally "safe"?

Method-1:

Method-2:

Is this understanding flawed?

amorey commented 2 months ago

This library implements the "signed double-submit cookie pattern" with a modification that instead of using a shared server-side secret, it uses a unique per-session secret (stored in a cookie). The advantage of this is that it doesn't require the developer to define an environment secret. The token is signed with the secret and both the secret (via cookie) and the token (via payload) must be present in the request and pass validation as described in the "signed double-submit cookie" pattern. Here are the relevant lines in the source code for reference: https://github.com/kubetail-org/edge-csrf/blob/main/shared/src/protect.ts#L123-L146

With regards to "Method-1", if CORS is disabled then a GET /csrf route cannot be used by an attacker to forge a request from a third-party site (at least not by any method I can think of).

With regards to "Method-2", it sounds like you're describing a pattern that retrieves the token from the initial HTTP response. If so, then that will also be safe but you should keep in mind that typically cookies in the "double-submit cookie pattern" have HttpOnly enabled.

Going back to your original question, the main advantage of using a GET /csrf route in the Next.js example is that it allows the GET / route to be statically optimized at build time because the response HTML doesn't change. If this isn't important to you then you can use this example to include the csrf token in a <meta> tag in the response HTML: https://github.com/kubetail-org/edge-csrf/tree/main/examples/next14-approuter-js-submission-dynamic

justin0108 commented 2 months ago

thanks @amorey for your prompt response. Appreciate your effort in developing this library for NextJS