Cross-Site Scripting (XSS): The Complete Practical Guide

How to Prevent XSS

How to prevent XSS: context-aware output encoding, framework auto-escaping and its escape hatches, strict CSP, Trusted Types, and HttpOnly cookies.

Published

Every technique in this cluster exists because output reached the browser without being encoded for where it landed. So the fix is not a clever filter, it is doing output correctly and layering defenses so a mistake is survivable. This page is the defensive side, written so you know what actually closes the bugs the rest of the guide opens.

Context-aware output encoding (the real fix)

The core fix is to encode data for the exact context it is written into, at the moment it is written:

  • HTML body: HTML-encode so < becomes &lt; and your value renders as text.
  • HTML attribute: attribute-encode and always quote the attribute, so a value cannot add a new attribute or close the tag.
  • JavaScript string: JavaScript-encode, or better, do not build script from data at all, pass it as JSON read from a data attribute.
  • URL: URL-encode, and reject dangerous schemes like javascript: in link targets.
  • CSS: CSS-encode values placed into styles.

The point is that there is no single "escape" that is correct everywhere. A value HTML-encoded for the body is still dangerous dropped into a javascript: URL. One global htmlentities() call is not enough, because the right encoding depends on the sink. Encode at the sink, for the sink.

Let the framework escape by default

Modern frameworks escape interpolated values automatically, which removes most of the easy mistakes. The risk is concentrated in the escape hatches that turn that protection off:

React:   dangerouslySetInnerHTML
Angular: [innerHTML]  (and bypassSecurityTrust...)
Vue:     v-html
Django:  |safe / mark_safe

Every one of those says "render this as raw HTML." Treat each occurrence as a thing to justify and audit, and never pass it untrusted data without a parser-accurate sanitizer in front. This directly answers the common belief that a framework makes you immune: it protects the default path, not the escape hatches.

A strict, nonce-based CSP

A Content Security Policy is defense in depth, not a fix, but a good one turns many injections into non-events. Aim for:

Content-Security-Policy:
  script-src 'nonce-<random>' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

No unsafe-inline, a fresh random nonce per response, and explicit object-src and base-uri so the omission bypasses do not apply. The policy should make a lone injected script useless even when encoding somewhere else failed.

Trusted Types for DOM sinks

Client-side bugs like DOM-based XSS come from untrusted strings reaching sinks like innerHTML and eval. Trusted Types lets you tell the browser to reject plain strings at those sinks and accept only values that passed a vetted policy function. It moves DOM-XSS prevention from "remember to sanitize every sink" to "the browser enforces it," which is a much stronger position.

HttpOnly and SameSite cookies

These do not prevent XSS, they blunt its payoff. HttpOnly means a stolen document.cookie yields nothing, forcing an attacker to act from the session rather than lift the cookie. SameSite limits cross-site request abuse. Set both on session cookies so that even a successful injection has less to work with.

Parser-accurate HTML sanitization for rich text

When you genuinely must render user-supplied HTML (comments with formatting, a WYSIWYG field), use a maintained, parser-accurate sanitizer with an allow-list of tags and attributes, and keep it updated. Never hand-roll regex sanitization, and be aware of mutation XSS, where markup is rewritten after sanitizing. Sanitize as late as possible and do not round-trip the result back through innerHTML.

Why input filtering alone fails

Blocking "bad" characters on input is tempting and unreliable. The same input is safe in one context and dangerous in another, so input filtering cannot know what is safe without knowing the eventual sink. Legitimate data often needs the characters you would block (names with apostrophes, code snippets with angle brackets). Validate input for business reasons, but close XSS on output.

The defensive summary is short: encode on output for the context, let the framework escape and audit the escape hatches, layer a strict CSP and Trusted Types, and set HttpOnly cookies. To understand what you are defending against, go break it on a free live target.

← Back to Cross-Site Scripting (XSS): The Complete Practical Guide