[{"data":1,"prerenderedAt":338},["ShallowReactive",2],{"blog-how-to/implement-csrf-protection":3},{"id":4,"title":5,"body":6,"category":318,"date":319,"dateModified":320,"description":321,"draft":322,"extension":323,"faq":324,"featured":322,"headerVariant":325,"image":324,"keywords":324,"meta":326,"navigation":327,"ogDescription":328,"ogTitle":324,"path":329,"readTime":324,"schemaOrg":330,"schemaType":331,"seo":332,"sitemap":333,"stem":334,"tags":335,"twitterCard":336,"__hash__":337},"blog/blog/how-to/implement-csrf-protection.md","How to Implement CSRF Protection",{"type":7,"value":8,"toc":297},"minimark",[9,13,17,21,30,35,38,49,53,72,75,86,90,93,99,103,106,121,134,147,151,155,158,164,168,174,178,184,188,191,197,201,204,210,214,220,224,278],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-implement-csrf-protection",[18,19,20],"p",{},"Prevent unauthorized actions from malicious websites",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR",[18,28,29],{},"Use SameSite=Lax or Strict cookies for session tokens. For APIs using cookies, add CSRF tokens to forms and verify them server-side. If you use Bearer tokens in Authorization headers (not cookies), you likely don't need CSRF protection.",[31,32,34],"h2",{"id":33},"what-is-csrf","What is CSRF?",[18,36,37],{},"Cross-Site Request Forgery happens when a malicious website tricks your browser into making requests to another site where you're logged in. Because cookies are sent automatically, the malicious request appears legitimate.",[39,40,45],"pre",{"className":41,"code":43,"language":44},[42],"language-text","\u003C!-- Malicious website has this hidden form -->\n\u003Cform action=\"https://yourbank.com/transfer\" method=\"POST\">\n  \u003Cinput type=\"hidden\" name=\"to\" value=\"attacker-account\">\n  \u003Cinput type=\"hidden\" name=\"amount\" value=\"10000\">\n\u003C/form>\n\u003Cscript>document.forms[0].submit();\u003C/script>\n\n\u003C!-- If you're logged into yourbank.com, your session cookie\n     is sent automatically with this request! -->\n","text",[46,47,43],"code",{"__ignoreMap":48},"",[31,50,52],{"id":51},"do-you-need-csrf-protection","Do You Need CSRF Protection?",[54,55,56,59],"info-box",{},[18,57,58],{},"You DON'T need CSRF tokens if:",[60,61,62,66,69],"ul",{},[63,64,65],"li",{},"You use Bearer tokens in Authorization headers (not cookies)",[63,67,68],{},"Your API only accepts JSON with proper Content-Type headers",[63,70,71],{},"You use SameSite=Strict cookies",[18,73,74],{},"You DO need CSRF protection if:",[60,76,77,80,83],{},[63,78,79],{},"You use session cookies for authentication",[63,81,82],{},"Your forms use traditional form submissions",[63,84,85],{},"You need to support older browsers without SameSite support",[31,87,89],{"id":88},"method-1-samesite-cookies-simplest","Method 1: SameSite Cookies (Simplest)",[18,91,92],{},"The easiest protection is using SameSite cookies. This prevents cookies from being sent on cross-origin requests.",[39,94,97],{"className":95,"code":96,"language":44},[42],"// Set cookie with SameSite attribute\nres.cookie('session', sessionId, {\n  httpOnly: true,\n  secure: process.env.NODE_ENV === 'production',\n  sameSite: 'lax', // or 'strict' for maximum protection\n  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days\n});\n\n// SameSite options:\n// 'strict' - Cookie only sent for same-site requests\n// 'lax' - Cookie sent for same-site + top-level navigations (links)\n// 'none' - Cookie sent for all requests (requires secure: true)\n",[46,98,96],{"__ignoreMap":48},[31,100,102],{"id":101},"method-2-csrf-tokens-traditional","Method 2: CSRF Tokens (Traditional)",[18,104,105],{},"Generate a random token, store it in the session, and require it in forms.",[107,108,110,115],"step",{"number":109},"1",[111,112,114],"h3",{"id":113},"install-a-csrf-library","Install a CSRF library",[39,116,119],{"className":117,"code":118,"language":44},[42],"npm install csrf\n",[46,120,118],{"__ignoreMap":48},[107,122,124,128],{"number":123},"2",[111,125,127],{"id":126},"generate-and-verify-tokens","Generate and verify tokens",[39,129,132],{"className":130,"code":131,"language":44},[42],"import Tokens from 'csrf';\n\nconst tokens = new Tokens();\n\n// Generate a secret (store in session)\nconst secret = tokens.secretSync();\n\n// Generate a token for the form\nconst csrfToken = tokens.create(secret);\n\n// Verify the token\nconst isValid = tokens.verify(secret, submittedToken);\n",[46,133,131],{"__ignoreMap":48},[107,135,137,141],{"number":136},"3",[111,138,140],{"id":139},"include-token-in-forms","Include token in forms",[39,142,145],{"className":143,"code":144,"language":44},[42],"\u003Cform action=\"/api/transfer\" method=\"POST\">\n  \u003Cinput type=\"hidden\" name=\"_csrf\" value=\"{{csrfToken}}\">\n  \u003Cinput type=\"text\" name=\"amount\">\n  \u003Cbutton type=\"submit\">Transfer\u003C/button>\n\u003C/form>\n",[46,146,144],{"__ignoreMap":48},[31,148,150],{"id":149},"nextjs-implementation","Next.js Implementation",[111,152,154],{"id":153},"server-actions-app-router","Server Actions (App Router)",[18,156,157],{},"Next.js Server Actions have built-in CSRF protection. The framework validates that the request came from your application.",[39,159,162],{"className":160,"code":161,"language":44},[42],"// This is protected automatically\n'use server';\n\nexport async function transferMoney(formData: FormData) {\n  const amount = formData.get('amount');\n  // Process transfer\n}\n",[46,163,161],{"__ignoreMap":48},[111,165,167],{"id":166},"api-routes-with-cookies","API Routes with Cookies",[39,169,172],{"className":170,"code":171,"language":44},[42],"// lib/csrf.ts\nimport Tokens from 'csrf';\nimport { cookies } from 'next/headers';\n\nconst tokens = new Tokens();\n\nexport function generateCsrfToken() {\n  const secret = tokens.secretSync();\n  const token = tokens.create(secret);\n\n  // Store secret in cookie\n  cookies().set('csrf-secret', secret, {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'lax',\n  });\n\n  return token;\n}\n\nexport function verifyCsrfToken(token: string): boolean {\n  const secret = cookies().get('csrf-secret')?.value;\n  if (!secret) return false;\n  return tokens.verify(secret, token);\n}\n\n// app/api/transfer/route.ts\nimport { verifyCsrfToken } from '@/lib/csrf';\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n\n  if (!verifyCsrfToken(body._csrf)) {\n    return Response.json(\n      { error: 'Invalid CSRF token' },\n      { status: 403 }\n    );\n  }\n\n  // Process the request\n}\n",[46,173,171],{"__ignoreMap":48},[31,175,177],{"id":176},"expressjs-implementation","Express.js Implementation",[39,179,182],{"className":180,"code":181,"language":44},[42],"import express from 'express';\nimport csrf from 'csurf';\nimport cookieParser from 'cookie-parser';\n\nconst app = express();\napp.use(cookieParser());\napp.use(express.urlencoded({ extended: false }));\n\n// Enable CSRF protection\nconst csrfProtection = csrf({ cookie: true });\n\n// Apply to state-changing routes\napp.post('/transfer', csrfProtection, (req, res) => {\n  // Token is verified automatically\n  // If invalid, middleware returns 403\n  res.json({ success: true });\n});\n\n// Provide token to forms\napp.get('/form', csrfProtection, (req, res) => {\n  res.render('form', { csrfToken: req.csrfToken() });\n});\n",[46,183,181],{"__ignoreMap":48},[31,185,187],{"id":186},"double-submit-cookie-pattern","Double Submit Cookie Pattern",[18,189,190],{},"An alternative that doesn't require server-side session storage.",[39,192,195],{"className":193,"code":194,"language":44},[42],"// Set CSRF cookie (readable by JavaScript)\nres.cookie('csrf-token', token, {\n  httpOnly: false, // Must be readable by JS\n  secure: true,\n  sameSite: 'strict',\n});\n\n// Client reads cookie and sends in header\nfetch('/api/transfer', {\n  method: 'POST',\n  headers: {\n    'X-CSRF-Token': getCookie('csrf-token'),\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify(data),\n});\n\n// Server compares cookie value with header value\nconst cookieToken = req.cookies['csrf-token'];\nconst headerToken = req.headers['x-csrf-token'];\nif (cookieToken !== headerToken) {\n  return res.status(403).json({ error: 'CSRF validation failed' });\n}\n",[46,196,194],{"__ignoreMap":48},[31,198,200],{"id":199},"for-spas-with-json-apis","For SPAs with JSON APIs",[18,202,203],{},"If your API only accepts JSON and uses proper CORS, you get protection automatically:",[39,205,208],{"className":206,"code":207,"language":44},[42],"// Only accept JSON content type\napp.use((req, res, next) => {\n  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {\n    const contentType = req.headers['content-type'];\n    if (!contentType?.includes('application/json')) {\n      return res.status(415).json({ error: 'Content-Type must be application/json' });\n    }\n  }\n  next();\n});\n\n// Combined with SameSite cookies, this blocks CSRF attacks\n// because browsers can't send JSON with custom Content-Type cross-origin\n",[46,209,207],{"__ignoreMap":48},[31,211,213],{"id":212},"testing-csrf-protection","Testing CSRF Protection",[39,215,218],{"className":216,"code":217,"language":44},[42],"# Test without CSRF token (should fail)\ncurl -X POST http://localhost:3000/api/transfer \\\n  -H \"Cookie: session=your-session-cookie\" \\\n  -d \"amount=100\"\n# Expected: 403 Forbidden\n\n# Test with CSRF token (should succeed)\ncurl -X POST http://localhost:3000/api/transfer \\\n  -H \"Cookie: session=your-session-cookie\" \\\n  -H \"X-CSRF-Token: valid-token\" \\\n  -d \"amount=100\"\n# Expected: 200 OK\n",[46,219,217],{"__ignoreMap":48},[31,221,223],{"id":222},"quick-reference","Quick Reference",[225,226,227,243],"table",{},[228,229,230],"thead",{},[231,232,233,237,240],"tr",{},[234,235,236],"th",{},"Auth Method",[234,238,239],{},"CSRF Risk",[234,241,242],{},"Protection",[244,245,246,258,269],"tbody",{},[231,247,248,252,255],{},[249,250,251],"td",{},"Cookie sessions",[249,253,254],{},"High",[249,256,257],{},"SameSite + CSRF tokens",[231,259,260,263,266],{},[249,261,262],{},"Bearer tokens (localStorage)",[249,264,265],{},"None",[249,267,268],{},"Not needed (but XSS risk)",[231,270,271,274,276],{},[249,272,273],{},"Bearer tokens (httpOnly cookie)",[249,275,254],{},[249,277,257],{},[279,280,281,287,292],"related-articles",{},[282,283],"related-card",{"description":284,"href":285,"title":286},"Step-by-step guide to implementing TOTP-based two-factor authentication. Add 2FA with Google Authenticator, backup codes","/blog/how-to/two-factor-auth","How to Implement Two-Factor Authentication (2FA)",[282,288],{"description":289,"href":290,"title":291},"Step-by-step guide to validating user input. Zod schemas, server-side validation, common validation patterns, and why cl","/blog/how-to/validate-user-input","How to Validate User Input Securely",[282,293],{"description":294,"href":295,"title":296},"Step-by-step guide to setting up HashiCorp Vault for secrets management. Store, access, and rotate secrets securely in y","/blog/how-to/vault-basics","How to Use HashiCorp Vault for Secrets Management",{"title":48,"searchDepth":298,"depth":298,"links":299},2,[300,301,302,303,309,313,314,315,316,317],{"id":33,"depth":298,"text":34},{"id":51,"depth":298,"text":52},{"id":88,"depth":298,"text":89},{"id":101,"depth":298,"text":102,"children":304},[305,307,308],{"id":113,"depth":306,"text":114},3,{"id":126,"depth":306,"text":127},{"id":139,"depth":306,"text":140},{"id":149,"depth":298,"text":150,"children":310},[311,312],{"id":153,"depth":306,"text":154},{"id":166,"depth":306,"text":167},{"id":176,"depth":298,"text":177},{"id":186,"depth":298,"text":187},{"id":199,"depth":298,"text":200},{"id":212,"depth":298,"text":213},{"id":222,"depth":298,"text":223},"how-to","2026-01-19","2026-02-04","Step-by-step guide to implementing CSRF protection in Next.js and Express. Token-based protection, SameSite cookies, and when you actually need CSRF tokens.",false,"md",null,"yellow",{},true,"CSRF protection in Next.js and Express. Tokens, SameSite cookies, and best practices.","/blog/how-to/implement-csrf-protection","[object Object]","HowTo",{"title":5,"description":321},{"loc":329},"blog/how-to/implement-csrf-protection",[],"summary_large_image","tgQ7z4ZzOjSEKfWoVbD7iXEqB94Ry8vQ7fuzblYqF3I",1775843928190]