All articles
Vulnerabilities11 min readJanuary 6, 2026
Input ValidationSanitizationZodTypeScript

Input Validation and Sanitization: The Security Basics AI Ignores

AI generates code that trusts user input. Learn why validation is essential and how to implement it properly.

Security Guide

The Cardinal Rule AI Breaks

Never trust user input.

AI generates code that directly uses request data:

javascript
// AI-generated (VULNERABLE)
const user = await db.users.create({
  email: req.body.email,        // Could be anything
  name: req.body.name,          // Could be malicious
  age: req.body.age,            // Could be a string, negative, etc.
})

Without validation, attackers can:

  • Inject malicious code
  • Bypass business rules
  • Corrupt your database
  • Crash your application

Validation vs. Sanitization

Validation: Checking if input meets criteria

javascript
// Validation: Is this a valid email?
if (!email.includes('@')) throw new Error('Invalid email')

Sanitization: Cleaning input to remove dangerous content

javascript
// Sanitization: Remove HTML tags
const clean = input.replace(/<[^>]*>/g, '')

You need both.

Schema Validation with Zod

Zod is the standard for TypeScript validation:

bash
npm install zod

Basic Usage

typescript
import { z } from 'zod'

const UserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().min(0).max(150), })

// In your API route export async function POST(req: Request) { const body = await req.json()

const result = UserSchema.safeParse(body)

if (!result.success) { return Response.json( { error: 'Validation failed', details: result.error.issues }, { status: 400 } ) }

// result.data is typed and validated const user = await db.users.create(result.data) return Response.json(user) }

Common Validations

typescript
// Strings
z.string()
z.string().email()
z.string().url()
z.string().uuid()
z.string().min(1).max(255)
z.string().regex(/^[a-zA-Z0-9_]+$/)

// Numbers z.number() z.number().int() z.number().positive() z.number().min(0).max(100)

// Booleans z.boolean()

// Dates z.date() z.string().datetime()

// Enums z.enum(['admin', 'user', 'guest'])

// Arrays z.array(z.string()) z.array(z.string()).min(1).max(10)

// Objects z.object({ id: z.string().uuid(), tags: z.array(z.string()), })

// Optional and nullable z.string().optional() // string | undefined z.string().nullable() // string | null z.string().nullish() // string

null
undefined

// Default values z.string().default('guest') z.number().default(0)

// Transform z.string().transform(s => s.toLowerCase()) z.string().transform(s => s.trim())

API Route Validation Pattern

typescript
// lib/validate.ts
import { z, ZodSchema } from 'zod'

export async function validateRequest( request: Request, schema: ZodSchema ): Promise<{ data: T } | { error: Response }> { try { const body = await request.json() const data = schema.parse(body) return { data } } catch (error) { if (error instanceof z.ZodError) { return { error: Response.json( { error: 'Validation failed', issues: error.issues }, { status: 400 } ), } } return { error: Response.json( { error: 'Invalid request body' }, { status: 400 } ), } } }

// Usage in API route const CreatePostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(1).max(10000), published: z.boolean().default(false), })

export async function POST(request: Request) { const result = await validateRequest(request, CreatePostSchema)

if ('error' in result) { return result.error }

const post = await db.posts.create({ data: result.data, })

return Response.json(post) }

Query Parameter Validation

typescript
const QuerySchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
  sort: z.enum(['asc', 'desc']).default('desc'),
  search: z.string().max(100).optional(),
})

export async function GET(request: Request) { const { searchParams } = new URL(request.url)

const result = QuerySchema.safeParse({ page: searchParams.get('page'), limit: searchParams.get('limit'), sort: searchParams.get('sort'), search: searchParams.get('search'), })

if (!result.success) { return Response.json({ error: 'Invalid query parameters' }, { status: 400 }) }

const { page, limit, sort, search } = result.data // Use validated and typed parameters }

Sanitization Strategies

HTML Sanitization

typescript
import DOMPurify from 'isomorphic-dompurify'

// Allow safe HTML (for rich text) const cleanHtml = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], ALLOWED_ATTR: ['href'], })

// Strip all HTML const plainText = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [], ALLOWED_ATTR: [], })

SQL-Safe Values

typescript
// DON'T sanitize - use parameterized queries
const query = SELECT * FROM users WHERE id = ${sanitize(id)} // WRONG

// DO use parameters const query = 'SELECT * FROM users WHERE id = $1' await db.query(query, [id]) // RIGHT

Filename Sanitization

typescript
function sanitizeFilename(filename: string): string {
  return filename
    .replace(/[^a-zA-Z0-9.-]/g, '_')  // Replace unsafe chars
    .replace(/\.{2,}/g, '.')          // Remove path traversal
    .slice(0, 255)                     // Limit length
}

// Usage const safeFilename = sanitizeFilename(userFilename) const path = uploads/${safeFilename}

Common Validation Patterns

Email Addresses

typescript
const emailSchema = z.string()
  .email('Invalid email address')
  .max(255)
  .transform(email => email.toLowerCase().trim())

Passwords

typescript
const passwordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .max(128, 'Password is too long')
  .regex(/[A-Z]/, 'Must contain uppercase letter')
  .regex(/[a-z]/, 'Must contain lowercase letter')
  .regex(/[0-9]/, 'Must contain number')

URLs

typescript
const urlSchema = z.string()
  .url('Invalid URL')
  .refine(
    url => url.startsWith('https://'),
    'URL must use HTTPS'
  )

Phone Numbers

typescript
const phoneSchema = z.string()
  .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number')

Monetary Values

typescript
const priceSchema = z.number()
  .positive('Price must be positive')
  .multipleOf(0.01, 'Invalid price format')
  .max(999999.99, 'Price too high')

Error Handling

Client-Friendly Errors

typescript
function formatZodErrors(error: z.ZodError) {
  return error.issues.map(issue => ({
    field: issue.path.join('.'),
    message: issue.message,
  }))
}

// Response { "error": "Validation failed", "issues": [ { "field": "email", "message": "Invalid email address" }, { "field": "age", "message": "Number must be greater than 0" } ] }

Don't Leak Internal Details

typescript
// WRONG - Exposes internal structure
return Response.json({ error: error.message }, { status: 500 })

// RIGHT - Generic message return Response.json({ error: 'Invalid request' }, { status: 400 })

Validation Checklist

EVERY API ENDPOINT:
===================
[ ] Request body validated with schema
[ ] Query parameters validated
[ ] URL parameters validated
[ ] File uploads validated (type, size)

VALIDATION RULES: ================= [ ] Required fields marked required [ ] String lengths limited [ ] Numbers have min/max bounds [ ] Enums for fixed value sets [ ] Regex for format validation

SANITIZATION: ============= [ ] HTML sanitized when needed [ ] Filenames cleaned [ ] Paths checked for traversal [ ] URLs validated for SSRF

ERROR HANDLING: =============== [ ] Validation errors return 400 [ ] Errors don't expose internals [ ] Field-level error messages

The Bottom Line

AI generates code that trusts everything. You must add validation to every input: request bodies, query parameters, URL parameters, and file uploads.

Validate first, process after. Never the other way around.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free