Protect Against CSRF with Double Submit
Use a same-site cookie plus a double-submit token so state-changing requests cannot be forged from another origin.
Task
Add a double-submit CSRF token paired with SameSite cookies to state-changing endpoints.
When to use this
A state-changing endpoint accepts a session cookie without an anti-CSRF token, so a cross-site form can act as the user.
Prerequisites
- A web framework with cookie access
- A front end that can read a cookie
- State-changing endpoints
Steps
1. Issue a CSRF cookie and require the same token in a header.
res.cookie('csrf', token, { sameSite: 'lax', secure: true, httpOnly: false });
app.use((req, res, next) => {
if (['POST','PUT','PATCH','DELETE'].includes(req.method)) {
const header = req.get('x-csrf-token');
if (!header || header !== req.cookies.csrf) return res.status(403).end();
}
next();
});Expected output: State-changing requests require a header that a cross-site page cannot read, blocking forged requests.
2. Set session cookies to SameSite.
res.cookie('session', sid, { httpOnly: true, secure: true, sameSite: 'strict' });Expected output: The session cookie is not sent on cross-site navigations, adding defence in depth.
Troubleshooting
Problem
Legitimate cross-site POST breaks
Solution
Use the token exchange flow or documented OAuth redirects rather than relaxing CSRF checks.
Problem
Token readable by scripts
Solution
The CSRF cookie must be readable by your origin scripts but not httpOnly; keep the session cookie httpOnly.
Limitations
- Double submit relies on cookie scoping being correct.
- Subdomain attacks can defeat it if cookies are not scoped tightly.
- Does not replace input validation.
What this recipe does
Add a double-submit CSRF token paired with SameSite cookies to state-changing endpoints.
A state-changing endpoint accepts a session cookie without an anti-CSRF token, so a cross-site form can act as the user.
Prerequisites
- A web framework with cookie access
- A front end that can read a cookie
- State-changing endpoints
Procedure
- Issue a CSRF cookie and require the same token in a header.
res.cookie('csrf', token, { sameSite: 'lax', secure: true, httpOnly: false });
app.use((req, res, next) => {
if (['POST','PUT','PATCH','DELETE'].includes(req.method)) {
const header = req.get('x-csrf-token');
if (!header || header !== req.cookies.csrf) return res.status(403).end();
}
next();
});
Expected result: State-changing requests require a header that a cross-site page cannot read, blocking forged requests.
- Set session cookies to SameSite.
res.cookie('session', sid, { httpOnly: true, secure: true, sameSite: 'strict' });
Expected result: The session cookie is not sent on cross-site navigations, adding defence in depth.
How to confirm it worked
Run the commands above and confirm every expected result in order. If a step does not produce the stated output, use the troubleshooting notes before continuing. A passing run shows a forged cross-site POST without the header is rejected 403 while same-origin requests with the token succeed.
Troubleshooting
Legitimate cross-site POST breaks — Use the token exchange flow or documented OAuth redirects rather than relaxing CSRF checks.
Token readable by scripts — The CSRF cookie must be readable by your origin scripts but not httpOnly; keep the session cookie httpOnly.
Limitations
- Double submit relies on cookie scoping being correct.
- Subdomain attacks can defeat it if cookies are not scoped tightly.
- Does not replace input validation.