Open justin0108 opened 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);
};
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?
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
thanks @amorey for your prompt response. Appreciate your effort in developing this library for NextJS
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"):
and middleware.ts
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