Server-side safety
Never trust submitted input
Treat every field, filename, header, and hidden value as untrusted data and validate it against the server’s own rules.
The form you wrote is not a security boundary. The server receives an HTTP request, with no proof it came from your page or from a browser at all.
Anyone with curl can:
- omit required fields
- repeat a field several times
- add fields that don’t exist in the page
- change hidden and disabled values
- skip browser validation entirely
- send a different content type
- upload arbitrary bytes under a familiar filename
So the server can’t ask “did the form allow this?”. It has to ask “do I allow this?”.
Start with a contract
Before writing the endpoint, write down what it accepts. For a profile update: displayName is a string of 1 to 80 characters, timezone is one value from a known list. Nothing else. Fields you didn’t list are ignored or rejected. Every line of the contract becomes one check.
Validate in layers
Cheap checks first, so a bad request is rejected before it costs anything:
- Check that the request body is small enough to parse safely.
- Accept only the expected content type.
- Parse with a maintained parser.
- Check required fields and data types.
- Check lengths, ranges, and allowed values.
- Check relationships and business rules.
- Check that the authenticated user may perform the operation.
A 50 MB body is refused at step 1, before a single field is read.
Syntax is not meaning
2026-07-30 is a well-formed date. That doesn’t make the room available that night. A productId may exist and still belong to another account. Format checks say “this could be a real value”. Business checks say “this value is acceptable right now, for this person”. The second kind always needs the database.
Validation doesn’t replace safe output
A value that passed every check is still text a stranger typed. Escape it for the HTML context where you display it. Pass it to the database as a query parameter, never by concatenating it into the SQL string. Validation limits what gets in. Escaping and parameterized queries limit what it can do.
Don’t accept what you already know
The current user comes from the session, not from a userId field. The price comes from the products table, not from a hidden input. A submitted ownerId or price is at best a claim to verify.
For ordinary mistakes, return a clear field error. For requests that are impossible from your page, log them: someone is probing. Keep passwords and tokens out of the log.
Try this: pick one endpoint and write its contract on paper. Then send one request that breaks each rule and check that every one is rejected.
Lesson completed