You render a comment on your page. Someone typed <script>alert(1)</script> into the box. If that string hits the DOM raw, congratulations — you've got a cross-site scripting hole. Escaping HTML entities is the fix, and it's simple once you understand what problem it's actually solving.
What HTML escaping is
Browsers treat certain characters as markup. The < starts a tag, & starts an entity, and quotes delimit attribute values. Escaping means replacing those special characters with their entity equivalents so the browser renders them as literal text instead of interpreting them as code.
The core set most people care about:
&becomes&<becomes<>becomes>"becomes"'becomes'(or')
Escape the ampersand first. If you don't, you'll double-escape everything else — your < turns into &lt; and shows up as garbage on screen.
When you actually need it
The rule of thumb: escape data at the point where it enters HTML, not when it enters your database. Store the raw value; escape on output. This keeps your data clean and lets you reuse it in contexts where HTML escaping would be wrong (like a JSON API or a plain-text email).
You need escaping whenever untrusted or user-controlled data lands in HTML markup:
- Rendering usernames, comments, or search terms back onto a page
- Displaying content from a database that originated from user input
- Building HTML strings server-side from external data
- Showing code samples or logs that may contain angle brackets
That last one catches people. Even trusted content — a tutorial showing HTML tags, for instance — needs escaping so the tags display instead of executing. If you've ever tried to write about <div> in a blog post, you already know this pain.
Context matters more than you think
HTML escaping is not one-size-fits-all. The correct escaping depends on where in the document the value goes.
Element content vs attributes
Inside element text, escaping <, >, and & is usually enough. Inside an attribute, quotes matter too. And crucially — always quote your attributes.
<!-- Fine if you escape and quote -->
<img alt=""quoted" text" src="...">
<!-- Dangerous: unquoted attribute -->
<img alt=user_value src="...">With an unquoted attribute, an attacker can inject a space and add their own attributes like onerror=, and standard HTML escaping won't save you. Quote everything.
Other contexts need other encoding
HTML escaping does nothing useful in these places:
- JavaScript strings — you need JS string escaping, not HTML entities
- URLs — use percent-encoding (
encodeURIComponent) - CSS values — CSS has its own escaping rules
Putting HTML-escaped data into a <script> block or an href won't protect you. Match the encoding to the context.
Common gotchas
Double escaping. Running an already-escaped string through an escaper again gives you visible &lt; in the output. This happens when template engines auto-escape and you also escape manually. Know whether your framework escapes by default.
Assuming your template auto-escapes everything. Most modern engines (Jinja2, ERB, JSX, Blade) escape by default in element content. But nearly all of them offer a "raw" or "safe" escape hatch — {{ x | safe }}, dangerouslySetInnerHTML, Html.Raw(). The moment you use one, you own the escaping responsibility.
Escaping too early. If you escape before storing, then export that data to a CSV or JSON API, consumers get mangled values full of &lt;. Store raw, escape on render.
Forgetting non-HTML sinks. The classic: escaping HTML but then dropping the value into element.innerHTML via JavaScript with additional concatenation. Use textContent when you just want text.
A short worked example
Say you're building a comment display server-side. A user submits:
Nice article! <script>steal()</script> & thanksNaive rendering drops that script straight into the page. A minimal JavaScript escaper looks like this:
function escapeHtml(str) {
return str
.replace(/&/g, "&") // must be first
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
escapeHtml('Nice article! <script>steal()</script> & thanks');
// => "Nice article! <script>steal()</script> & thanks"Now the browser renders the tags as visible text. The script never runs, the ampersand shows correctly, and the comment reads exactly as typed.
In the browser you can often skip the manual work entirely — assigning to textContent escapes for you:
const el = document.getElementById('comment');
el.textContent = userInput; // safe, no manual escaping neededThe manual version is what you reach for when you're building HTML strings, working server-side, or need to inspect exactly what's being encoded.
Quick escaping without writing code
When you're debugging, prepping a code snippet for docs, or checking what a string should look like escaped, spinning up a script is overkill. I keep our HTML Escape Tool bookmarked for exactly this — paste in a string, get the escaped output, and unescape when you need to go the other way. It's faster than opening a console and remembering the replace order.
The short version
- Escape data on output into HTML, not on input into storage
- Escape the ampersand first to avoid double encoding
- Always quote HTML attributes
- HTML escaping is not JS, URL, or CSS escaping — match the context
- Know when your framework auto-escapes, and be careful with "raw" escape hatches
Get these right and a whole category of XSS bugs simply stops happening. It's not glamorous work, but it's the kind of thing that separates code that survives a security review from code that doesn't.
Photo by Danial Igdery on Unsplash