Input Validation
Validate every byte of user input on the server before it touches your data or logic.
Why
Every input is an attack surface. The username field, the search query, the file upload, the URL parameter, the JSON body, the cookie value — if any of them reaches your database, your template renderer, or your command shell without validation, it is an injection vector. Input validation is the single most effective control against injection (SQL, NoSQL, command, template, log), XSS, path traversal, and a long tail of business-logic bugs (negative quantities, future dates, strings where numbers are expected). The rule is simple: never trust the client, validate on the server, and reject early — a request that fails validation should never reach the database query or the template render.
When
Validate every external input: HTTP body, query params, path params, headers, cookies, file uploads, messages from other services. Validate at the system boundary (the API handler), before the data reaches any service or repository.
How
1. Define a schema for every input using a validation library
- Zod (TS, inferred types), Joi (Node), Pydantic (Python), serde (Rust)
- The schema is the single source of truth for the input shape
2. Validate at the API boundary, before any business logic
- Parse the input with the schema; if it fails, return 400 with a clear error
- Never pass raw req.body to a service — pass the validated, typed object
- Use the inferred type so the compiler enforces the validated shape downstream
3. Validate the things you would rather not think about
- Types: string where number expected, array where single object expected
- Ranges: negative price, future birthdate, quantity > available stock
- Lengths: 10MB string in a username field, 10000-element array
- Formats: email, URL, UUID, date — use format validators, not regex you wrote
4. Sanitize for the output context, not at input
- Do not HTML-escape at input — you do not yet know the output context (HTML, JSON, SQL)
- Escape at the output: parameterized queries for SQL, framework auto-escape for HTML
- Input validation is about type and range; output encoding is about injection
5. Test the validation itself
- For each field, test: valid value passes, wrong type rejected, out-of-range rejected,
too long rejected, empty rejected (if required), malicious payload rejected
- Fuzz the input with random/edge-case data to find validation gapsExample
// src/schemas/order.ts — Zod schema, single source of truth
import { z } from "zod";
export const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100), // no negative, no bulk abuse
couponCode: z.string().regex(/^[A-Z0-9]{4,20}$/).optional(),
shippingDate: z.string().datetime().refine(
(d) => new Date(d) > new Date(), // no past dates
{ message: "shipping date must be in the future" },
),
});
export type CreateOrder = z.infer<typeof CreateOrderSchema>;
// src/routes/orders.ts — validate at the boundary
import { CreateOrderSchema } from "../schemas/order";
router.post("/orders", async (req, res) => {
const parsed = CreateOrderSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.issues });
}
const order: CreateOrder = parsed.data; // typed, validated, safe to pass on
await orderService.create(order); // service never sees raw input
});
// Test the validation
it("rejects negative quantity", () => {
const res = CreateOrderSchema.safeParse({ productId: "x", quantity: -5 });
expect(res.success).toBe(false);
});Common Problems
- ⚠Validating on the client (HTML required attribute, JS form check) but not the server — the client validation is a UX convenience, and any attacker bypasses it with a curl call directly to the API.
- ⚠Using a regex written by hand for email/URL/date validation — the regex is almost certainly wrong (email RFC 5322 is a 6000-character regex), and a library validator is both correct and maintained.
- ⚠Validating type but not range or length — the field is a number (type check passes) but it is -999999, or a string but it is 10MB, and the downstream code crashes or behaves pathologically.
- ⚠HTML-escaping at input and then double-escaping at output, producing &lt; in the rendered page — escape at the output context, not the input, to avoid both injection and double-encoding.
Best Practices
- ✓Use a schema library (Zod, Pydantic) that gives you both validation and a static type — the validated object flows through your code with compiler-enforced types, eliminating a whole class of "wrong shape" bugs.
- ✓Validate at the system boundary (API handler, message consumer) and pass only the validated typed object downstream — services and repositories should never see raw external input.
- ✓Validate type, range, length, and format for every field — a field that accepts any string of any length is a field that will be abused, even if the abuse is not an injection but a resource exhaustion.
- ✓Test the validation schema independently of the handler — for each field, assert that valid values pass and that wrong type, out-of-range, too long, and malicious payloads are rejected.