Skip to content

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

  1. 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.
  2. Normalize before you validate. Decode, lowercase, resolve .., trim — then check. Checking first and decoding later is how %2e%2e/ walks out of a folder.
  3. 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 name

Fail 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.