--- name: input-validation-xss-prevention description: Validate and sanitize user input to prevent XSS, injection attacks, and ensure data quality. Use this skill when you need to validate forms, sanitize user input, prevent cross-site scripting, use Zod schemas, or handle any user-generated content. Triggers include "input validation", "validate input", "XSS", "cross-site scripting", "sanitize", "Zod", "injection prevention", "validateRequest", "safeTextSchema", "user input security". --- # Input Validation & XSS Prevention ## The Universal Truth of Web Security **Never trust user input.** This is the foundational principle of web security. Every major breach can be traced back to input validation failures: - **SQL Injection** - Equifax (147 million records) - **XSS** - British Airways (380,000 transactions, £20M fine) - **Command Injection** - Countless others According to OWASP, injection vulnerabilities are consistently the **#1 or #2 threat** to web applications. Input validation is not optional—it's existential. ## Understanding XSS (Cross-Site Scripting) ### The Attack Attacker enters in a bio field: ```javascript ``` Without sanitization, when other users view this profile: 1. The script executes in their browsers 2. It steals their user data 3. Sends it to attacker's server 4. Victims never know they were compromised ### Real-World XSS Consequences **British Airways (2018):** XSS vulnerability allowed attackers to inject payment card harvesting script. 380,000 transactions compromised. **£20 million fine** under GDPR. **MySpace Samy Worm (2005):** XSS vulnerability allowed a self-propagating script that added the attacker as a friend to over 1 million profiles in 20 hours. While mostly harmless (just adding friends), it demonstrated the potential: the same technique could have stolen credentials or payment data. ## Our Input Validation Architecture ### Why Zod? Traditional validation uses regular expressions and manual checks—error-prone and often incomplete. **Zod provides:** - ✅ **Type-safe validation** - TypeScript knows what's valid - ✅ **Composable schemas** - Reuse validation logic - ✅ **Automatic transformation** - Sanitization built-in - ✅ **Clear error messages** - Helps users fix mistakes - ✅ **Runtime type checking** - Catches issues in production ### The Sanitization Strategy We remove dangerous characters that enable XSS attacks: - `<` - Prevents opening tags - `>` - Prevents closing tags - `"` - Prevents attribute injection - `&` - Prevents HTML entity injection **Preserved:** - `'` - Apostrophes (for names like O'Neal, D'Angelo, McDonald's) **Why not remove all special characters?** Because then users named "O'Neal" can't enter their names. Security must balance safety with usability. ### Industry Validation Approach According to OWASP and NIST guidelines, the secure approach is: 1. **Validate** (check format/type) 2. **Sanitize** (remove dangerous content) 3. **Encode on output** (escape when displaying) We do all three: - Zod validates format - `.transform()` sanitizes - React escapes output ## Implementation Files - `lib/validation.ts` - 11 pre-built Zod schemas - `lib/validateRequest.ts` - Validation helper that formats errors ## How to Use Input Validation ### Basic Pattern ```typescript import { validateRequest } from '@/lib/validateRequest'; import { safeTextSchema } from '@/lib/validation'; async function handler(request: NextRequest) { const body = await request.json(); // Validate and sanitize const validation = validateRequest(safeTextSchema, body); if (!validation.success) { return validation.response; // Returns 400 with field errors } // TypeScript knows exact shape, data is XSS-sanitized const sanitizedData = validation.data; // Safe to use } ``` ### Complete Secure API Route ```typescript // app/api/create-post/route.ts import { NextRequest, NextResponse } from 'next/server'; import { withRateLimit } from '@/lib/withRateLimit'; import { withCsrf } from '@/lib/withCsrf'; import { validateRequest } from '@/lib/validateRequest'; import { createPostSchema } from '@/lib/validation'; import { handleApiError, handleUnauthorizedError } from '@/lib/errorHandler'; import { auth } from '@clerk/nextjs/server'; async function createPostHandler(request: NextRequest) { try { // Authentication const { userId } = await auth(); if (!userId) return handleUnauthorizedError(); const body = await request.json(); // Validation & Sanitization const validation = validateRequest(createPostSchema, body); if (!validation.success) { return validation.response; } const { title, content, tags } = validation.data; // Data is now: // - Type-safe (TypeScript validated) // - Sanitized (XSS characters removed) // - Validated (length, format checked) // Safe to store in database await db.posts.insert({ title, content, tags, userId, createdAt: Date.now() }); return NextResponse.json({ success: true }); } catch (error) { return handleApiError(error, 'create-post'); } } export const POST = withRateLimit(withCsrf(createPostHandler)); export const config = { runtime: 'nodejs', }; ``` ## Available Validation Schemas All schemas are in `lib/validation.ts`: ### 1. emailSchema **Use for:** Email addresses ```typescript import { emailSchema } from '@/lib/validation'; const validation = validateRequest(emailSchema, userEmail); if (!validation.success) return validation.response; const email = validation.data; // Normalized, lowercase ``` **Features:** - Valid email format required - Normalized to lowercase - Max 254 characters - Trims whitespace ### 2. safeTextSchema **Use for:** Short text fields (names, titles, subjects) ```typescript import { safeTextSchema } from '@/lib/validation'; const validation = validateRequest(safeTextSchema, inputText); ``` **Features:** - Min 1, max 100 characters - Removes: `< > " &` - Preserves: `'` (apostrophes) - Trims whitespace ### 3. safeLongTextSchema **Use for:** Long text (descriptions, bios, comments, messages) ```typescript import { safeLongTextSchema } from '@/lib/validation'; const validation = validateRequest(safeLongTextSchema, description); ``` **Features:** - Min 1, max 5000 characters - Same sanitization as safeTextSchema - Suitable for textarea content ### 4. usernameSchema **Use for:** Usernames, slugs, identifiers ```typescript import { usernameSchema } from '@/lib/validation'; const validation = validateRequest(usernameSchema, username); ``` **Features:** - Alphanumeric + underscores + hyphens only - Min 3, max 30 characters - Lowercase only - No spaces or special characters ### 5. urlSchema **Use for:** Website URLs, link fields ```typescript import { urlSchema } from '@/lib/validation'; const validation = validateRequest(urlSchema, websiteUrl); ``` **Features:** - Must be valid URL - HTTPS only (security requirement) - Max 2048 characters - Validates protocol, domain ### 6. contactFormSchema **Use for:** Complete contact forms ```typescript import { contactFormSchema } from '@/lib/validation'; const validation = validateRequest(contactFormSchema, formData); if (!validation.success) return validation.response; const { name, email, subject, message } = validation.data; ``` **Fields:** ```typescript { name: string, // safeTextSchema (1-100 chars) email: string, // emailSchema subject: string, // safeTextSchema (1-100 chars) message: string // safeLongTextSchema (1-5000 chars) } ``` ### 7. createPostSchema **Use for:** User-generated blog posts, articles ```typescript import { createPostSchema } from '@/lib/validation'; const validation = validateRequest(createPostSchema, postData); if (!validation.success) return validation.response; const { title, content, tags } = validation.data; ``` **Fields:** ```typescript { title: string, // safeTextSchema (1-100 chars) content: string, // safeLongTextSchema (1-5000 chars) tags: string[] | null // Array of safeText strings (optional) } ``` ### 8. updateProfileSchema **Use for:** Profile updates ```typescript import { updateProfileSchema } from '@/lib/validation'; const validation = validateRequest(updateProfileSchema, profileData); if (!validation.success) return validation.response; const { displayName, bio, website } = validation.data; ``` **Fields:** ```typescript { displayName: string | null, // safeTextSchema (optional) bio: string | null, // safeLongTextSchema (optional) website: string | null // urlSchema (optional, HTTPS only) } ``` ### 9. idSchema **Use for:** Database IDs, reference fields ```typescript import { idSchema } from '@/lib/validation'; const validation = validateRequest(idSchema, itemId); ``` **Features:** - Non-empty string - Trims whitespace - Use for validating ID parameters ### 10. positiveIntegerSchema **Use for:** Counts, quantities, pagination ```typescript import { positiveIntegerSchema } from '@/lib/validation'; const validation = validateRequest(positiveIntegerSchema, quantity); ``` **Features:** - Integer only - Must be positive (> 0) - No decimals ### 11. paginationSchema **Use for:** Pagination parameters ```typescript import { paginationSchema } from '@/lib/validation'; const validation = validateRequest(paginationSchema, { page: queryParams.page, limit: queryParams.limit }); const { page, limit } = validation.data; ``` **Fields:** ```typescript { page: number, // Default: 1, Min: 1 limit: number // Default: 10, Min: 1, Max: 100 } ``` ## Creating Custom Schemas ### Custom Schema Template ```typescript // lib/validation.ts import { z } from 'zod'; // Add your custom schema export const myCustomSchema = z.object({ field: z.string() .min(1, 'Required') .max(200, 'Too long') .trim() .transform((val) => val.replace(/[<>"&]/g, '')), // XSS sanitization }); export type MyCustomData = z.infer; ``` ### Complex Schema Example ```typescript // Registration form with multiple validations export const registrationSchema = z.object({ username: usernameSchema, email: emailSchema, password: z.string() .min(12, 'Password must be at least 12 characters') .regex(/[A-Z]/, 'Must contain uppercase letter') .regex(/[a-z]/, 'Must contain lowercase letter') .regex(/[0-9]/, 'Must contain number') .regex(/[^A-Za-z0-9]/, 'Must contain special character'), passwordConfirm: z.string(), agreeToTerms: z.boolean().refine(val => val === true, { message: 'You must agree to terms' }) }).refine((data) => data.password === data.passwordConfirm, { message: "Passwords don't match", path: ["passwordConfirm"] }); ``` ### Conditional Validation ```typescript export const orderSchema = z.object({ orderType: z.enum(['pickup', 'delivery']), address: z.string().optional(), phone: z.string().optional() }).refine( (data) => { if (data.orderType === 'delivery') { return !!data.address && !!data.phone; } return true; }, { message: 'Address and phone required for delivery', path: ['address'] } ); ``` ## Frontend Validation ### Client-Side Pre-validation ```typescript 'use client'; import { useState } from 'react'; import { createPostSchema } from '@/lib/validation'; export function CreatePostForm() { const [errors, setErrors] = useState>({}); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setErrors({}); const formData = new FormData(e.currentTarget); const data = { title: formData.get('title'), content: formData.get('content'), tags: formData.get('tags')?.toString().split(',').filter(Boolean) || null }; // Client-side validation (UX improvement, not security) const validation = createPostSchema.safeParse(data); if (!validation.success) { const fieldErrors: Record = {}; validation.error.errors.forEach((err) => { if (err.path[0]) { fieldErrors[err.path[0].toString()] = err.message; } }); setErrors(fieldErrors); return; } // Submit to server (server validates again!) try { const response = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(validation.data) }); if (response.ok) { alert('Post created!'); } } catch (error) { console.error('Error:', error); } } return (
{errors.title && {errors.title}}