Input validation guide
Skill VersoXBT/claude-initial-setup/skills/security/input-validation-guide
Validate and sanitize all user input using schema validation libraries. Activate whenever the user writes form handlers, API endpoints, request parsers, data processing functions, or any code that accepts external input. Also activate when the user asks about validation libraries like Zod, Joi, or Pydantic, or discusses input sanitization.From its SKILL.md
npx -y skills add VersoXBT/claude-initial-setup --skill input-validation-guideAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.7 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it
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-Typeheaders 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
| Language | Library | Key Pattern |
|---|---|---|
| TypeScript | Zod | schema.safeParse(input) returns { success, data, error } |
| TypeScript | Joi | schema.validate(input) returns { value, error } |
| Python | Pydantic | Class-based models with type annotations |
| Go | validator | Struct tags: validate:"required,email" |
| 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 |
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most quality gates skills give in ~1.8k tokens
Counted across 1,524 of the 2,830 authors here whose files we hold, read 2026-09-06
- Read full output and check exit codein 45 of 1524, across 40 files
- Verify output confirms the claimin 44 of 1524, across 39 files
- Identify the command that proves the claimin 43 of 1524, across 39 files
- Execute the full verification commandin 36 of 1524, across 30 files
- Produce a verification reportin 34 of 1524, across 18 files
- Review git diff changesin 30 of 1524, across 16 files
- Fix build failures immediatelyin 29 of 1524, across 9 files
- Group findings by severityin 28 of 1524
- State claim only with evidencein 27 of 1524, across 22 files
- Verify regression tests with red-green cyclein 26 of 1524, across 22 files
- Run the full test suitein 26 of 1524, across 25 files
- Run test suite with coveragein 25 of 1524, across 10 files
Said here and by no other author read
- Validate all external input at system boundaries
- Use schema validation libraries for all input
- Reject invalid data early with clear error messages
- Prefer allowlists over denylists for filtering
- Parse raw input into typed domain objects
- Apply context-appropriate sanitization after validation
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.