Lesson 1 of 9 · 15m
Trust boundaries: every input is hostile until proven otherwise
Map where untrusted data enters, and apply validate → normalize → encode in the right places.
Most breaches are not exotic. Someone sent a value your code didn't expect, and your code believed it. Secure coding starts with one habit: know where untrusted data enters, and never let it cross a boundary without a check.
What counts as untrusted
Anything a person outside your process can influence:
- Query strings, form fields, JSON bodies, headers (including
Host,X-Forwarded-For,Referer) and cookies. - File uploads — the name, the declared type and the bytes.
- Webhooks and third-party APIs. "It came from Stripe" is only true after you verify the signature.
- Data you stored earlier from a user. A bio saved last year is still attacker-controlled when you render it today.
- Environment the attacker can reach: filenames in shared folders, DNS answers, redirects.
A trust boundary is any point where data moves from one of those sources into code that assumes it is well-formed: a SQL query, a shell, an HTML page, a file path, a URL fetch.
Validate, normalize, encode
- Validate on input with an allowlist: say what is allowed (
^[a-z0-9_]{3,24}$for a username) instead of trying to list what isn't. Denylists ("strip<script>") always miss a variant. - Normalize before you validate. Decode, lowercase, resolve
.., trim — then check. Checking first and decoding later is how%2e%2e/walks out of a folder. - Encode on output for the exact context: HTML text, HTML attribute, URL, SQL, shell. Validation narrows input; encoding is what actually stops injection.
import re
USERNAME = re.compile(r"[a-z0-9_]{3,24}")
def clean_username(raw: str) -> str:
name = raw.strip().lower() # normalize first
if not USERNAME.fullmatch(name): # then allowlist-validate
raise ValueError("invalid username")
return nameFail closed, and say little
When a check fails, reject the request. Don't "fix it up" and carry on, and don't echo internal details (stack traces, SQL, file paths) back to the client. Log the details server-side, return a short generic error.
Validate on the server
Browser checks (pattern=, maxlength, JavaScript) are a UX feature. Anyone can send the request with curl. Every rule that matters must run on the server too.
Practice: Escape It Before You Render It and Defuse a Spreadsheet Export.