Skip to main content
Harden/Chapter 51 · Validation

Trusting No Input

Share

Share this page

Pass it to someone who needs it.

Your app behaves while you are the only one using it, because you type exactly what it expects. Then a real user, another service, or an old file hands it something you never planned for: a blank field, text where a number belonged, a string a mile long. Now it crashes, or worse, quietly saves garbage. This chapter gives you the one habit that stops bad data at the door.

7.4.1Everything outside your code is untrusted

Draw a line around the code you wrote. Everything inside it, you control. Everything that crosses in from outside, you do not: what a user types, what another service returns, what a file or a URL carries. That crossing point is a boundary, and every value arriving at one is untrusted until you check it.

Untrusted does not mean the sender is hostile. It means you cannot assume the value is present, the right type, or a sane size. OWASP's input validation guidance treats this as a first line of defense, because a large share of real breaches begin with input nobody checked.

7.4.2Validate at the boundary with a schema

Validation is checking a value against a set of rules before your code trusts it. Do it once, right at the boundary, so nothing behind it ever handles a value that was not checked.

The clean way is a schema: one description of the exact shape you expect, field by field. A schema library turns that description into a checker you run on the raw input, and Zod is the common one for TypeScript:

import { z } from "zod";

// The shape you expect at the boundary:
const SignupSchema = z.object({
  email: z.string().email(),
  age: z.number().int().positive(),
  name: z.string().min(1).max(80),
});

// Untrusted input arriving in the request body:
const result = SignupSchema.safeParse(req.body);

if (!result.success) {
  // Stop here: return a clear rejection.
  return sendError(422, result.error);
}

// result.data is the correct shape, guaranteed.
const signup = result.data;

7.4.3Fail fast, with a clear error

When a value fails the schema, stop right there. Do not patch it, guess what the user meant, or let it slide half-checked into the app. Reject it, and say exactly what broke.

Failing fast means the error surfaces at the boundary, where you still know the field name and the rule it broke, instead of three functions deeper as a crash nobody can place. Return it in the error shape you already designed for your API: a stable code, a human message, the right status.

Watch out: The failure that hurts is not a loud crash, it is silent coercion: turning bad input into 0, null, or an empty string and carrying on. That is how corrupt data gets saved. Reject, never coerce.

7.4.4Always validate on the server

Checks in the browser make the form pleasant: instant red text, no round trip. They are for UX, and on their own they stop nothing. Anyone can open the developer tools, hit your endpoint directly with curl, or send a crafted request that never touches your form.

So the rule is absolute: validate on the server, every time, even when the client already did. The server is the only code you truly control. Client checks are a courtesy; the server check is the wall.

This prompt hands every boundary to your agent:

Act as a senior engineer hardening how my app
validates input. List every boundary where
untrusted data enters: form submissions, URL
and query params, request bodies, uploaded
files, and responses from third-party APIs.
For each, write a schema (use Zod if my stack
is TypeScript, otherwise the standard validator
for my language) pinning the exact type, range,
and format I expect. Parse the input against it
at the boundary, before any logic runs. On
failure, reject fast with a clear, specific
error in my API's error shape, never coerce or
swallow it. Put every check on the server and
treat client-side validation as UX only. Start
with my riskiest boundary and show the schema
plus its reject path first.

My app's inputs and boundaries:

Do this now: paste the prompt, let your agent map your boundaries and write a schema for each, then wire every one to reject bad input on the server before your code trusts it.

Discussion

Questions, ideas, and feedback on this chapter.

Mahmoud Zalt

Mahmoud Zalt

Software engineer, 16+ yrs · built Sistava.com in 3 months, idea to production, using these methods

ExploreBook a call
Companion RepoContribute
Support me

Support my work

A small tip keeps the free work coming.

© 2026 Mahmoud Zalt. Free to read, not to republish. Copyright & license