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.md
source 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
    Content-Type
    headers without verifying actual content
  • 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

LanguageLibraryKey Pattern
TypeScriptZod
schema.safeParse(input)
returns
{ success, data, error }
TypeScriptJoi
schema.validate(input)
returns
{ value, error }
PythonPydanticClass-based models with type annotations
GovalidatorStruct tags:
validate:"required,email"
PrincipleRule
Validate earlyAt system boundaries, not deep in business logic
AllowlistPermit known-good, reject everything else
Parse, don't validateTransform raw input into typed domain objects
Fail loudlyReturn specific field-level error messages
Never trust clientServer-side validation is mandatory