Claude-initial-setup input-validation-guide
install
source · Clone the upstream repo
git clone https://github.com/VersoXBT/claude-initial-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/security/input-validation-guide" ~/.claude/skills/versoxbt-claude-initial-setup-input-validation-guide && rm -rf "$T"
manifest:
skills/security/input-validation-guide/SKILL.mdsource content
Input Validation Guide
Validate all external input at system boundaries using schema validation libraries. Reject invalid data early with clear error messages. Never trust client-side validation as the sole defense.
When to Use
- Writing API endpoint handlers that accept request bodies, query params, or path params
- Building form validation logic (server-side)
- Processing data from external APIs or file uploads
- Parsing configuration files or environment variables
- Any function that receives data from outside the trust boundary
Core Patterns
Zod (TypeScript)
The standard for TypeScript schema validation. Parse, don't validate.
import { z } from 'zod'; // Define schemas as the source of truth const CreateUserSchema = z.object({ email: z.string().email('Invalid email format'), name: z.string().min(1, 'Name required').max(100), age: z.number().int().min(13, 'Must be 13 or older').max(150), role: z.enum(['user', 'admin', 'moderator']).default('user'), website: z.string().url().optional(), }); // Infer TypeScript types from schemas type CreateUserInput = z.infer<typeof CreateUserSchema>; // Parse at the boundary, use typed data internally app.post('/api/users', async (req, res) => { const result = CreateUserSchema.safeParse(req.body); if (!result.success) { return res.status(400).json({ error: 'Validation failed', issues: result.error.issues.map(i => ({ field: i.path.join('.'), message: i.message, })), }); } // result.data is fully typed as CreateUserInput const user = await createUser(result.data); res.json(user); });
Advanced Zod patterns for complex validation:
// Transform and refine const SearchParamsSchema = 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(['name', 'date', 'relevance']).default('relevance'), q: z.string().trim().min(1).max(200).optional(), }); // Discriminated unions for type-safe variants const PaymentSchema = z.discriminatedUnion('method', [ z.object({ method: z.literal('card'), cardNumber: z.string().regex(/^\d{16}$/), expiry: z.string().regex(/^\d{2}\/\d{2}$/), cvv: z.string().regex(/^\d{3,4}$/), }), z.object({ method: z.literal('bank_transfer'), accountNumber: z.string().min(8).max(20), routingNumber: z.string().length(9), }), ]); // Custom refinements with cross-field validation const DateRangeSchema = z.object({ startDate: z.coerce.date(), endDate: z.coerce.date(), }).refine( data => data.endDate > data.startDate, { message: 'End date must be after start date', path: ['endDate'] } );
Pydantic (Python)
The standard for Python data validation using type annotations.
from pydantic import BaseModel, Field, field_validator, EmailStr from datetime import date from enum import Enum class UserRole(str, Enum): USER = "user" ADMIN = "admin" MODERATOR = "moderator" class CreateUserRequest(BaseModel): email: EmailStr name: str = Field(min_length=1, max_length=100) age: int = Field(ge=13, le=150) role: UserRole = UserRole.USER website: str | None = None @field_validator("name") @classmethod def name_must_not_be_empty(cls, v: str) -> str: stripped = v.strip() if not stripped: raise ValueError("Name cannot be blank") return stripped # FastAPI integration - validation is automatic @app.post("/api/users") async def create_user(data: CreateUserRequest): # data is already validated and typed return await save_user(data)
Allowlists vs Denylists
Always prefer allowlists. Denylists are inherently incomplete.
// WRONG: Denylist approach - always has gaps function sanitizeFilename(name: string): string { return name.replace(/[<>:"/\\|?*]/g, ''); // What about null bytes? Unicode tricks? } // CORRECT: Allowlist approach - only permit known-good characters function sanitizeFilename(name: string): string { const sanitized = name.replace(/[^a-zA-Z0-9._-]/g, ''); if (!sanitized || sanitized.startsWith('.')) { throw new Error('Invalid filename'); } return sanitized; } // CORRECT: Allowlist for content types const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf']); function validateContentType(type: string): boolean { return ALLOWED_TYPES.has(type); }
Type Coercion Attack Prevention
Prevent attacks that exploit JavaScript's loose type coercion.
// WRONG: Vulnerable to type coercion app.get('/api/users', (req, res) => { // req.query.admin could be "true" (string), true (if parsed), or ["true"] (array) if (req.query.admin == true) { // Loose equality - dangerous return getAdminData(); } }); // CORRECT: Strict parsing with Zod const QuerySchema = z.object({ admin: z.enum(['true', 'false']).transform(v => v === 'true').optional(), id: z.coerce.number().int().positive(), // Explicit coercion }); app.get('/api/users', (req, res) => { const query = QuerySchema.parse(req.query); // query.admin is boolean | undefined, query.id is number }); // WRONG: JSON.parse without validation const config = JSON.parse(userInput); // Could be any type // CORRECT: Parse then validate const rawData = JSON.parse(userInput); const config = ConfigSchema.parse(rawData);
Sanitization for Specific Contexts
Apply context-appropriate sanitization after validation.
import DOMPurify from 'dompurify'; import sqlstring from 'sqlstring'; // HTML content: strip dangerous tags function sanitizeHtml(input: string): string { return DOMPurify.sanitize(input, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'li'], ALLOWED_ATTR: [], }); } // Path traversal prevention function safePath(basedir: string, userPath: string): string { const resolved = path.resolve(basedir, userPath); if (!resolved.startsWith(basedir)) { throw new Error('Path traversal detected'); } return resolved; }
Anti-Patterns
- Relying solely on client-side validation (easily bypassed)
- Using denylists instead of allowlists for filtering
- Validating deep inside business logic instead of at the boundary
- Using loose equality (
) instead of strict equality (==
) in JavaScript=== - Trusting
headers without verifying actual contentContent-Type - Silently coercing invalid data instead of rejecting it
- Writing custom regex for email, URL, or date validation instead of using library validators
- Catching validation errors and returning generic "Bad Request" without field-level detail
Quick Reference
| Language | Library | Key Pattern |
|---|---|---|
| TypeScript | Zod | returns |
| TypeScript | Joi | returns |
| Python | Pydantic | Class-based models with type annotations |
| Go | validator | Struct tags: |
| Principle | Rule |
|---|---|
| Validate early | At system boundaries, not deep in business logic |
| Allowlist | Permit known-good, reject everything else |
| Parse, don't validate | Transform raw input into typed domain objects |
| Fail loudly | Return specific field-level error messages |
| Never trust client | Server-side validation is mandatory |