DEV Community

jiebang-tools
jiebang-tools

Posted on

HTML Entity Encoding in JavaScript: A Practical Guide with Real Examples

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
< &lt; Less-than, tag delimiter
> &gt; Greater-than, tag delimiter
& &amp; Ampersand, entity delimiter
" &quot; Double quote
' &#39; Single quote (numeric)
(space) &nbsp; Non-breaking space

You can also use numeric encoding:

  • Decimal: &#60; for <
  • Hexadecimal: &#x3C; 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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;'
  };
  return str.replace(/[&<>"']/g, char => escapeMap[char]);
}

const userInput = '<script>alert("XSS")</script>';
const safe = escapeHTML(userInput);
// Result: &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
Enter fullscreen mode Exit fullscreen mode

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: &lt;img src=x onerror=alert(1)&gt;
Enter fullscreen mode Exit fullscreen mode

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('&lt;hello&gt; &amp; &quot;world&quot;');
// Result: <hello> & "world"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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: &lt;div&gt;

// Accidentally encoding again
const doubleEncoded = escapeHTML(encoded);
// Result: &amp;lt;div&amp;gt;  ← Page shows &lt;div&gt; instead of <div>
Enter fullscreen mode Exit fullscreen mode

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>`;
Enter fullscreen mode Exit fullscreen mode

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 : '#';
}
Enter fullscreen mode Exit fullscreen mode

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: &lt;script&gt;

// Option 2: String replacement (same as Method 1)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 textContent for 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 he library

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)