Chapter 1 · Practice 2 of 4
Validate every input against an allowlist schema at the boundary
Why it matters
OWASP describes two layers of validation:
- Syntactic: the value has the right type and shape (an integer, a UUID, an ISO date, a string of at most 200 characters).
- Semantic: the value makes sense here (the end date is after the start date, the quantity is positive, the currency is one you support).
Write both as an allowlist: describe what is acceptable and reject everything else. OWASP is direct about denylists: blocking quotes or angle brackets does not make a query or a page safe, so "do not try to recognize every malicious string".
A schema library keeps this declarative and in one place. Parse the request into a typed object at the edge, reject unknown fields, and pass only the parsed value inward. Code deeper in the stack can then trust the type it receives.
Validation is not your injection or XSS defense. OWASP's cheat sheet says validation reduces attack surface, but parameterized queries and output encoding are what actually stop injection. Do both.
Example
import { z } from 'zod';
const CreateOrder = z
.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
note: z.string().max(500).optional(),
deliverAfter: z.coerce.date(),
})
.strict() // unknown keys are an error, not silently kept
.refine((o) => o.deliverAfter > new Date(), { message: 'must be in the future' });
export function parseCreateOrder(body: unknown) {
return CreateOrder.parse(body); // throws on anything outside the allowlist
}