If you've ever seen & or < on a webpage and wondered what they mean, or had your site vulnerable to XSS because of unescaped user input — this guide is for you.
HTML entity encoding is one of those fundamentals that separates developers who know security basics from those who don't. Let's break it down.
What Are HTML Entities?
HTML entities are a way to represent special characters in HTML documents. Some characters have special meaning in HTML — like < which starts a tag, or & which begins an entity reference. To display these characters literally, you need to encode them.
Common HTML entities:
| Character | Entity | Description |
|---|---|---|
< |
< |
Less-than, tag delimiter |
> |
> |
Greater-than, tag delimiter |
& |
& |
Ampersand, entity delimiter |
" |
" |
Double quote |
' |
' |
Single quote (numeric) |
| (space) | |
Non-breaking space |
You can also use numeric encoding:
- Decimal:
<for< - Hexadecimal:
<for<
Encoding in JavaScript
JavaScript doesn't have a built-in HTML encoding function (escape() is deprecated and doesn't handle HTML entities). But we have several reliable approaches.
Method 1: String Replacement
The simplest approach — replace the 5 critical characters identified by OWASP:
function escapeHTML(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, char => escapeMap[char]);
}
const userInput = '<script>alert("XSS")</script>';
const safe = escapeHTML(userInput);
// Result: <script>alert("XSS")</script>
This covers all characters that matter for XSS prevention.
Method 2: DOM API
Leverage the browser's built-in escaping:
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
const safe = escapeHTML('<img src=x onerror=alert(1)>');
// Result: <img src=x onerror=alert(1)>
How it works: textContent assignment doesn't parse HTML, and innerHTML reading automatically converts special characters to entities.
Decoding HTML Entities
function unescapeHTML(str) {
const div = document.createElement('div');
div.innerHTML = str;
return div.textContent;
}
const decoded = unescapeHTML('<hello> & "world"');
// Result: <hello> & "world"
Security warning: The innerHTML decoding approach has potential XSS risks. In production, always pair with a sanitizer like DOMPurify.
textContent vs innerHTML: The Core Distinction
This is web security 101:
const el = document.getElementById('output');
const userInput = '<script>alert("XSS")</script>';
// ❌ Dangerous: parses <script> as actual HTML
el.innerHTML = userInput;
// ✅ Safe: renders as plain text, auto-escaped
el.textContent = userInput;
Rule: Always prefer textContent for displaying user input. Only use innerHTML when you genuinely need to render HTML structure — and only after sanitizing.
Common Pitfalls (and How to Avoid Them)
1. Double Encoding
const encoded = escapeHTML('<div>');
// Result: <div>
// Accidentally encoding again
const doubleEncoded = escapeHTML(encoded);
// Result: &lt;div&gt; ← Page shows <div> instead of <div>
Fix: Encode once, when data enters HTML context. Decode once, when reading back.
2. Unescaped Attribute Values
// ❌ Dangerous: user input can break out of the attribute
const html = `<a href="${userInput}">Link</a>`;
// If userInput = '" onclick="alert(1)', attribute injection!
// ✅ Correct: encode attribute values too
const html = `<a href="${escapeHTML(userInput)}">Link</a>`;
3. JavaScript Protocol in URLs
// Even with encoded attributes, this attack vector remains
const url = 'javascript:alert(1)';
// ✅ Validate the protocol
function safeURL(url) {
return /^https?:\/\//.test(url) ? url : '#';
}
4. Server-Side vs Client-Side
// No DOM API in Node.js
// Option 1: Use the `he` library
import { encode, decode } from 'he';
const encoded = encode('<script>');
// Result: <script>
// Option 2: String replacement (same as Method 1)
Practical Example: Safe Comment Rendering
function renderComment(username, content) {
const safeName = escapeHTML(username);
const safeContent = escapeHTML(content);
const time = new Date().toLocaleString('en-US');
return `
<div class="comment">
<span class="author">${safeName}</span>
<span class="time">${time}</span>
<p class="content">${safeContent}</p>
</div>
`;
}
// Even malicious input is rendered as harmless text
renderComment('hacker', '<img src=x onerror=alert(document.cookie)>');
// The onerror handler never fires
Quick Reference Table
| Scenario | Recommended Approach | Notes |
|---|---|---|
| Plain text rendering | textContent |
Auto-escaped, safest |
| Building HTML strings | escapeHTML() |
Manual 5-char replacement |
| Decoding entities |
DOMParser or he library |
Watch for XSS |
| Node.js environment |
he library |
No DOM available |
| Rich text rendering | DOMPurify + innerHTML
|
Must use a sanitizer |
Summary
- HTML entity encoding is the first line of defense against XSS — escape
& < > " ' - Always prefer
textContentfor user input — it's inherently safe - When building HTML strings, you must manually escape — including attribute values
- Be cautious when decoding — pair with DOMPurify in production
- In Node.js, use the
helibrary
If you want to test HTML entity encoding interactively, I've built a free online HTML entity encoder/decoder tool — 100% client-side, your data never leaves your browser.
Found this helpful? Follow me for more practical JavaScript guides. Questions? Drop a comment below!
Top comments (0)