TL;DR
Never trust user input. Validate on client for UX, validate on server for security. Use schema validation (Zod, Yup), sanitize for the output context, and reject rather than try to fix malformed input. These prompts help you implement proper input handling.
Schema Validation Setup
Set up schema-based input validation with Zod.
Framework: Next.js/Express/tRPC
For each form/endpoint, create schemas that:
- Define expected shape of input
- Validate types (string, number, boolean)
- Check constraints (min, max, regex)
- Transform data as needed
- Provide clear error messages
Example schema for user registration:
- email: valid email format
- password: 8+ chars, not common password
- username: 3-20 chars, alphanumeric only
- age: optional, number 13-120
Create:
- Reusable field validators (email, phone, url)
- Form schemas that compose field validators
- Server-side validation middleware
- Type inference for TypeScript
Show how to share schemas between client and server.
Server-Side Validation
Add server-side input validation to my API routes.
Current problem: API accepts any input without validation
For each endpoint:
- Define expected input schema
- Validate before processing
- Return 400 with clear errors if invalid
- Never trust client validation alone
Implementation for Next.js API/Express:
// Middleware approach const validateBody = (schema) => (req, res, next) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ errors: result.error.flatten() }); } req.validatedBody = result.data; next(); };
Also validate:
- Query parameters
- URL path parameters
- Headers (auth tokens, content-type)
- File uploads (type, size)
Client validation is for UX only: Attackers can bypass your JavaScript. Every input must be validated server-side. Client validation just makes the user experience better.
Sanitize HTML Content
Sanitize user-provided HTML content safely.
Use case: Comments, posts, or profiles with rich text
Using DOMPurify (recommended):
const DOMPurify = require('dompurify'); const { JSDOM } = require('jsdom'); const window = new JSDOM('').window; const purify = DOMPurify(window);
const sanitize = (dirty) => { return purify.sanitize(dirty, { ALLOWED_TAGS: 'p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'br', ALLOWED_ATTR: 'href', FORBID_TAGS: 'script', 'style', 'iframe', 'form', 'input', FORBID_ATTR: 'onclick', 'onerror', 'onload', 'style' }); };
Sanitize:
- On input (before storing)
- On output (before rendering)
- Both is safest
Never use regex to strip HTML tags - use a proper parser.
Validate URLs and Links
Validate user-provided URLs to prevent attacks.
Threats:
- javascript: URLs (XSS)
- data: URLs (XSS)
- file: URLs (local file access)
- Open redirect to malicious sites
Safe URL validation:
function isValidUrl(input) { try { const url = new URL(input); // Only allow http and https if (!['http:', 'https:'].includes(url.protocol)) { return false; } // Optional: Allowlist of domains // if (!allowedDomains.includes(url.hostname)) return false; return true; } catch { return false; } }
For redirects:
- Use allowlist of permitted redirect destinations
- Or only allow relative URLs (/path, not //evil.com)
- Never redirect to user-provided absolute URLs
Pro tip: Use TypeScript with Zod for end-to-end type safety. Your validated data gets the correct types automatically, catching errors at compile time.
Should I sanitize on input or output?
Output is more important because context matters. But sanitizing on input too provides defense in depth. Store data in a clean form, then encode appropriately for each output context.
What's the difference between validation and sanitization?
Validation checks if input matches expected format and rejects if not. Sanitization modifies input to remove dangerous content. Prefer validation (reject bad input) over sanitization (fix bad input).