Output Encoding

Chapter 6
Output Encoding — The Primary Defence
Context-aware escaping, encode-on-output discipline, and why it's the foundation

Now the defences — and the first one is the most important. Output encoding (also called output escaping) is the primary, foundational defence against XSS: transform untrusted data, at the moment it's written into a page, so the browser treats it as inert data rather than executable code. Get this right and the vast majority of XSS simply cannot occur, because nothing attacker-supplied is ever interpreted.

The Principle: Encode on Output, for the Context

Two words carry the whole chapter. "On output": encode at the point data is inserted into the page, not when it's received. "For the context": use the encoder matching where the data lands (Chapter 3 — HTML, attribute, JS, URL, CSS). Encoding converts the characters that would break out of the current context into safe equivalents the browser displays literally.

// the same value, HTML-body-encoded on output input: <script>alert(1)</script> output: &lt;script&gt;alert(1)&lt;/script&gt; // browser shows it as text, runs nothing
Why "on output," not "on input"? Encode late, store raw
A tempting alternative is to encode/clean data as it arrives and store the encoded form. This is inferior for several reasons: (1) you don't yet know the output context at input time — the same value may later appear in HTML, an attribute, a URL, and JSON, each needing different encoding; (2) encoded data in the database corrupts non-HTML uses (emails, APIs, PDFs, length checks) and causes double-encoding bugs (&amp;lt;); (3) it conflates storage with presentation. Best practice: store the raw, original input; encode at each output site for that site's context. Input validation still has a role (Chapter 7) but it is not the XSS defence — output encoding is.

The Five Encoders

ContextEncodeExample transform
HTML bodyHTML entity encode< > &&lt; &gt; &amp;
HTML attribute (quoted)attribute encodealso " '&quot; &#39;
JavaScript stringJS string encode"\x22, /\/, hex-escape
URL (parameter)URL encode + scheme allowlistencodeURIComponent; reject javascript:
CSS valueCSS encode / allowlistavoid; restrict to known-safe values

The non-negotiable rule from Chapter 3 holds: the encoder must match the context. HTML-encoding a value that lands in a JavaScript string or a URL scheme does not protect it.

In Practice: Don't Hand-Roll It

You rarely write these encoders yourself — and shouldn't. Use established, context-aware tools:

  • Template engines auto-encode by default — Handlebars {{value}}, EJS <%= value %>, Jinja/Twig, Razor. The raw forms ({{{value}}}, <%- %>) bypass it — those are the danger spots to audit.
  • Frameworks auto-encode interpolated values — React {value}, Vue {{ }}, Angular — covered in Chapter 9.
  • Libraries for manual cases — OWASP Java Encoder, the he library (Node), Python's markupsafe.
// Node example: HTML-encode for an HTML-body context const he = require("he"); res.send(`<p>Hello, ${he.encode(req.query.name)}</p>`); // safe

Encoding vs Sanitization — Not the Same Thing

A crucial distinction that the next chapter expands: encoding makes data display literally (good when the value should be shown as text — a username, a comment, a search term). Sanitization (Chapter 7) removes dangerous parts while keeping some markup functional (needed when the value is meant to be HTML — a rich-text comment). Use encoding by default; reach for sanitization only when you genuinely must render user-provided HTML.

Encoding is for text-as-data; if you encode HTML you wanted to keep, it shows as tags
If a blog comment should render bold from <b>, HTML-encoding it produces the literal text "<b>" on the page — correct for safety, wrong for the feature. That's the signal you have an HTML-output requirement, which needs sanitization, not encoding (or better, a non-HTML markup format like Markdown rendered safely). Don't respond to "the tags show up as text" by disabling encoding — that reintroduces XSS. The two tools solve different problems; pick by whether the value is text or markup.

Why Encoding Is the Foundation

Recall Chapter 4: blocklist filtering loses because the attacker has infinite payload variants. Output encoding wins for the mirror-image reason — it doesn't try to recognize bad input; it neutralizes a small, closed set of breakout characters for the current context, so every variant of every payload is rendered inert at once. It's complete by construction, not by enumeration. That's why it's the primary defence, with sanitization (Ch. 7) and CSP (Ch. 8) layered on top — defence in depth, but encoding is the load-bearing wall.

Hands-On Exercises

Exercise 1

Explain "encode on output, for the context" and give two concrete reasons encoding at output time is better than encoding/storing data at input time. Include the double-encoding and "unknown context" problems.

📄 View solution
Exercise 2

Take the value "><img src=x onerror=alert(1)> and show what correct encoding produces in (a) an HTML body context and (b) a quoted HTML attribute context. Confirm why neither executes, and what the browser displays.

📄 View solution
Exercise 3

Distinguish encoding from sanitization. For each requirement, say which to use and why: (a) display a user's chosen display-name; (b) render a rich-text comment that allows <b> and <a>; (c) show a search query back to the user. Explain why using the wrong tool either breaks the feature or reintroduces XSS.

📄 View solution

Chapter 6 Quick Reference

  • Output encoding = transform untrusted data on insertion so the browser treats it as data, not code — the primary XSS defence
  • Two rules: encode on output (at insertion, not input) and for the context (the right encoder for where it lands)
  • Store raw, encode late — input-time encoding causes double-encoding bugs & can't know the eventual context
  • Five encoders: HTML body · HTML attribute (quoted) · JS string · URL (+scheme allowlist) · CSS — must match the context
  • Don't hand-roll — use auto-encoding templates/frameworks (audit the raw escape hatches) or libraries (he, OWASP Encoder)
  • Encoding ≠ sanitization: encoding shows data literally (text); sanitization keeps safe markup (HTML) — use encoding by default
  • If encoded HTML "shows as tags," you have an HTML-output need → sanitize (Ch. 7); don't disable encoding
  • Encoding is complete by construction (closed set of breakout chars), the mirror of why blocklists fail — the foundation
  • Next chapter: sanitizing HTML & input validation — DOMPurify, allowlists, and where validation fits