How to Implement CSRF Protection
CSRF attacks trick authenticated users into making unwanted requests to your application. If a user is logged in to your banking app and visits a malicious site, that site could submit a transfer request on their behalf. This guide covers defense strategies.
Find security issues automatically before attackers do.
Follow These Steps
Use SameSite cookies as the first line of defense
SameSite cookies prevent most CSRF attacks by limiting when cookies are sent.
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax', // Sent with top-level navigations, blocked for cross-origin POSTs
maxAge: 86400000
})Use SameSite=Lax for most apps. Use SameSite=Strict only if your app does not use OAuth (OAuth redirects need Lax).
Check the Origin header on state-changing requests
Verify that the Origin header matches your domain for POST, PUT, DELETE requests.
function validateOrigin(req: Request): boolean {
const origin = req.headers.get('origin')
const allowedOrigins = ['https://yourdomain.com']
return !origin || allowedOrigins.includes(origin)
}
export async function POST(req: Request) {
if (!validateOrigin(req)) {
return Response.json({ error: 'Invalid origin' }, { status: 403 })
}
// Process request
}Implement CSRF tokens for traditional forms
For server-rendered forms, include a CSRF token that must match the server-side value.
import csrf from 'csurf'
// Express.js
const csrfProtection = csrf({ cookie: true })
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
app.post('/submit', csrfProtection, (req, res) => {
// CSRF token is automatically validated
res.json({ success: true })
})
// In HTML form
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<button type="submit">Submit</button>
</form>Use the Double Submit Cookie pattern for SPAs
For single-page apps, generate a CSRF token cookie and require it as a header on requests.
// Server: Set CSRF cookie on login
res.cookie('csrf-token', generateToken(), {
httpOnly: false, // JS needs to read this
secure: true,
sameSite: 'lax'
})
// Client: Read cookie and send as header
const csrfToken = document.cookie
.split('; ')
.find(c => c.startsWith('csrf-token='))
?.split('=')[1]
fetch('/api/action', {
method: 'POST',
headers: { 'X-CSRF-Token': csrfToken || '' }
})
// Server: Verify header matches cookie
function verifyCsrf(req) {
return req.cookies['csrf-token'] === req.headers['x-csrf-token']
}Verify protection is working
Test that cross-origin requests are properly blocked.
# Test from a different origin (should fail)
curl -X POST https://yourdomain.com/api/action \
-H "Origin: https://evil.com" \
-H "Cookie: session=..." \
-v
# Should return 403What You'll Achieve
Your application uses SameSite cookies, Origin header validation, and CSRF tokens to prevent cross-site request forgery attacks on all state-changing endpoints.
Common Mistakes to Avoid
Mistake
Using SameSite=None without the Secure flag
Fix
SameSite=None requires the Secure flag. Without it, the cookie will be rejected by browsers.
Mistake
Using SameSite=Strict with OAuth
Fix
Strict blocks the cookie on OAuth redirects. Use Lax for apps that use OAuth or any external redirects.
Mistake
Only protecting POST endpoints
Fix
Protect all state-changing methods: POST, PUT, PATCH, DELETE. GET requests should never modify state.
Frequently Asked Questions
Do SPAs need CSRF protection?
If your SPA uses cookie-based authentication (most do), yes. If you use Authorization headers with tokens stored in memory (not cookies), CSRF protection is not needed since the attacker cannot set custom headers.
Is SameSite=Lax enough?
SameSite=Lax prevents most CSRF attacks by blocking cross-origin POST requests with cookies. For additional defense, add Origin header validation. Full CSRF tokens are needed mainly for legacy browser support.
Ready to Secure Your App?
VAS automatically scans your deployed app for the security issues covered in this guide. Get actionable results in minutes.
Start Security Scan