FSD Frontend System Design

Part II β€” The Shield Β· Lesson 3.9

Input Validation and Sanitization

In one lineValidate shape on the way in, escape by context on the way out β€” they are different jobs.

Where you've seen itA rich-text comment editor that must preserve <b> and links while dropping <script> and every on* attribute.

  • Time15 min
  • DiagramInput β†’ validate β†’ store raw β†’ escape on output
  • Chapter3 Β· Security

Diagram

diagram
THE PIPELINE β€” store raw, transform at the boundary

  user input
      β”‚
      β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   reject if wrong TYPE, LENGTH, FORMAT, RANGE
  β”‚  VALIDATE    β”‚   "is this an email under 254 chars?"
  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   βœ— fail β†’ reject with a clear message
         β”‚           βœ“ pass
         β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   store the ORIGINAL, unescaped
  β”‚  PERSIST     β”‚   (escaping on input corrupts data and breaks
  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    the day you render it somewhere new)
         β”‚
         β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   HTML? URL? SQL? shell? each escapes differently
  β”‚  ESCAPE for  β”‚   this is where injection is actually prevented
  β”‚  the SINK    β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
diagram
VALIDATE vs SANITIZE β€” not interchangeable

  VALIDATE   accept or reject, unchanged      email, age, uuid, enum
             fail closed on anything unknown

  SANITIZE   accept and modify                rich text HTML only
             allowlist tags + attributes, drop everything else

  Use sanitization ONLY where markup is genuinely required.
  Everywhere else, validate and escape.
diagram
ALLOWLIST beats BLOCKLIST β€” always

  βœ— blocklist   strip "<script>"      β‡’ <ScRiPt>, <img onerror>, <svg onload>
                                        the bypass list is infinite

  βœ“ allowlist   keep [b,i,em,strong,a,p,ul,li]
                keep attrs [href, title]
                drop everything else, including anything invented next year

How it works

  1. Validate on both sides. Client for feedback, server as the gate β€” the client's copy can be bypassed entirely.
  2. Validate by type, not by pattern-matching bad input: schema libraries (Zod, Yup, JSON Schema) reject anything that doesn't fit.
  3. Store the raw value. Escaping at input time means you escape twice when it's later rendered into a different context.
  4. Escape at the sink β€” HTML-escape for the DOM, URL-encode for query strings, parameterise for SQL, never build shell commands from user data.
  5. Normalise before validating (trim, Unicode NFC) so equivalent inputs don't slip through a length or uniqueness check.

Trade-offs

RuleWhy
Fail closedUnknown input is rejected, not "cleaned"
One schema, both sidesClient and server can't drift apart
Limit length everywhereCheapest defence against payload and ReDoS abuse
Sanitize only rich textEvery other field just needs escaping

Interview angle

"You sanitize on input. Why is that not enough?"

  • The safe form depends on the destination β€” the same string needs different treatment in HTML, a URL, and SQL.
  • Escaping early corrupts the stored data and double-escapes when rendered elsewhere.
  • Correct order: validate shape on input, store raw, escape at each output sink.

Recap

  • Validation rejects; sanitization modifies. Only rich text needs the second.
  • Allowlists survive attacker creativity; blocklists don't.
  • Escape at the sink, because the sink defines what "safe" means.