XSS defense
Cross-site scripting happens when data the user controls is interpreted as
code by another user's browser. The defense is contextual output encoding:
the same string is safe in HTML text and dangerous in a <script> block, so
encoding depends on where the value lands. Autoescaping and a Content
Security Policy backstop the encoding you forget.
Method
- Encode at output, for the specific context. HTML body needs
HTML-entity encoding; an attribute needs attribute encoding; inside
<script> needs JavaScript-string encoding; a URL needs
percent-encoding. Encoding for the wrong context still lets script
through. Encode when you write to the page, not when you store.
- Keep template autoescaping on and never bypass it blindly. Jinja2,
Django, React (
{}), and Handlebars escape HTML by default. Audit every
bypass: |safe, mark_safe, dangerouslySetInnerHTML, v-html,
{{{ }}}. Each is a place you promised the value is already safe. Make
sure that promise holds.
- Sanitize rich HTML with a real library, not a regex. If users
legitimately submit HTML from a rich-text editor, run it through
DOMPurify or a server-side equivalent with a strict tag and attribute
allowlist. Regex stripping of
<script> is trivially bypassed with
<img onerror> and malformed tags.
- Guard URL sinks against
javascript: schemes. A user-supplied
href or src of javascript:alert(1) executes on click. After
canonicalizing, allow only http, https, and mailto; reject or
neutralize the rest.
- Deploy a strict Content Security Policy. Set
Content-Security-Policy with a nonce- or hash-based script-src, no
unsafe-inline, and no broad wildcards. CSP turns a missed encoding from
account takeover into a blocked script and a console error. Start in
Content-Security-Policy-Report-Only to find violations before you
enforce.
- Set session cookies HttpOnly so script cannot read them. Mark them
HttpOnly and Secure. Even if XSS fires, the token stays out of reach
of document.cookie, shrinking what a successful injection can steal.
Signals
- Can you name the output context (HTML, attribute, JS, URL) for every place
user data reaches the page?
- Does a grep for
dangerouslySetInnerHTML, |safe, and v-html turn up
only reviewed, justified uses?
- Does your CSP block inline script, tested against a real injection
payload?
Boundaries
Output encoding defends against XSS; it is not input validation and does not
stop injection into other sinks. DOM-based XSS lives entirely in client
JavaScript (innerHTML, eval, document.write) and needs the same
contextual encoding applied in the browser. Framework defaults cover most
cases, so reach for manual encoding only where you left autoescaping behind.
1---2name: xss-defense3description: Prevent cross-site scripting by encoding output for its exact sink, keeping template autoescaping on, and backing it with a strict Content Security Policy. Use when rendering user-controlled data into HTML, attributes, JavaScript, or URLs.4---56# XSS defense78Cross-site scripting happens when data the user controls is interpreted as9code by another user's browser. The defense is contextual output encoding:10the same string is safe in HTML text and dangerous in a `<script>` block, so11encoding depends on where the value lands. Autoescaping and a Content12Security Policy backstop the encoding you forget.1314## Method15161. **Encode at output, for the specific context.** HTML body needs17 HTML-entity encoding; an attribute needs attribute encoding; inside18 `<script>` needs JavaScript-string encoding; a URL needs19 percent-encoding. Encoding for the wrong context still lets script20 through. Encode when you write to the page, not when you store.212. **Keep template autoescaping on and never bypass it blindly.** Jinja2,22 Django, React (`{}`), and Handlebars escape HTML by default. Audit every23 bypass: `|safe`, `mark_safe`, `dangerouslySetInnerHTML`, `v-html`,24 `{{{ }}}`. Each is a place you promised the value is already safe. Make25 sure that promise holds.263. **Sanitize rich HTML with a real library, not a regex.** If users27 legitimately submit HTML from a rich-text editor, run it through28 DOMPurify or a server-side equivalent with a strict tag and attribute29 allowlist. Regex stripping of `<script>` is trivially bypassed with30 `<img onerror>` and malformed tags.314. **Guard URL sinks against `javascript:` schemes.** A user-supplied32 `href` or `src` of `javascript:alert(1)` executes on click. After33 canonicalizing, allow only `http`, `https`, and `mailto`; reject or34 neutralize the rest.355. **Deploy a strict Content Security Policy.** Set36 `Content-Security-Policy` with a nonce- or hash-based `script-src`, no37 `unsafe-inline`, and no broad wildcards. CSP turns a missed encoding from38 account takeover into a blocked script and a console error. Start in39 `Content-Security-Policy-Report-Only` to find violations before you40 enforce.416. **Set session cookies HttpOnly so script cannot read them.** Mark them42 `HttpOnly` and `Secure`. Even if XSS fires, the token stays out of reach43 of `document.cookie`, shrinking what a successful injection can steal.4445## Signals4647- Can you name the output context (HTML, attribute, JS, URL) for every place48 user data reaches the page?49- Does a grep for `dangerouslySetInnerHTML`, `|safe`, and `v-html` turn up50 only reviewed, justified uses?51- Does your CSP block inline script, tested against a real injection52 payload?5354## Boundaries5556Output encoding defends against XSS; it is not input validation and does not57stop injection into other sinks. DOM-based XSS lives entirely in client58JavaScript (`innerHTML`, `eval`, `document.write`) and needs the same59contextual encoding applied in the browser. Framework defaults cover most60cases, so reach for manual encoding only where you left autoescaping behind.