Escape HTML Special Characters
Inserting user input into HTML without escaping is the canonical XSS vector. The five characters `&<>"'` cover most rendering contexts, but attribute values, URL attributes, and `<script>` blocks each have stricter rules. This snippet starts with the minimal map every JS dev should memorise, adds an attribute-safe variant that also escapes the backtick, and ends with a note on when to reach for a real sanitiser like DOMPurify (without bundling it).
1,001 views
11
const HTML_ENTITIES = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
function escapeHtml(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>"']/g, (ch) => HTML_ENTITIES[ch]);
}
console.log(escapeHtml('<script>alert("xss")</script>'));
// <script>alert("xss")</script>
console.log(escapeHtml("Tom & Jerry's day"));
// Tom & Jerry's day
console.log(escapeHtml('plain text'));
// plain textThe five characters & < > " ' are the ones that change parsing in HTML text content and double-quoted attribute values. Replacing them with named entities (&, <, >, ") and one numeric entity (' for the single quote, since ' is not in HTML4) is enough for rendering server-generated text inside element bodies. Note that we escape & first by listing it first in the regex character class, but order inside [&<>"'] does not actually matter because each match is one character; the danger is hand-rolling sequential replace calls, where doing & last would double-encode the others. Use this function for plain text inserted between tags.
const ATTR_ENTITIES = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`',
'=': '=',
};
function escapeAttribute(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>"'`=]/g, (ch) => ATTR_ENTITIES[ch]);
}
// Demo: an unquoted attribute would let `=` and backtick close it early
const userInput = 'alt=evil onerror=alert(1) `';
console.log(`<img alt="${escapeAttribute(userInput)}">`);
// <img alt="alt=evil onerror=alert(1) `">Attribute context has more attack surface than text content because some browsers accept unquoted, single-quoted, double-quoted, and even backtick-delimited attributes. Adding the backtick (`) and equals (=) to the escape map defends against payloads that try to break out of an unquoted attribute by injecting evil onerror=alert(1). This still assumes you wrap the attribute in real quotes; if you concatenate values into unquoted attributes you have already lost. Pair this helper with double quotes around the attribute and you have a safe baseline for any string-into-attribute templating.
// This snippet does NOT execute DOMPurify; it shows the decision boundary.
function renderUntrustedText(str) {
// SAFE: text-only, escape and inject as textContent or {string}.
return escapeHtml(str);
}
// renderUntrustedHtml(str)
// UNSAFE TO ROLL YOUR OWN. Use a vetted library.
// import DOMPurify from 'dompurify';
// element.innerHTML = DOMPurify.sanitize(str, { USE_PROFILES: { html: true } });
// Decision rule:
// 1. The user is typing plain text (a name, a comment) -> escapeHtml.
// 2. The user is editing rich text (markdown -> HTML, WYSIWYG) -> DOMPurify.
// 3. The user controls a URL (href, src) -> URL parser + allowlist scheme.
// 4. The user controls JS or CSS strings -> don't allow it; refactor.
console.log(renderUntrustedText('<b>hi</b>'));
// <b>hi</b>escapeHtml only solves the case where you want the user's input to appear as literal text. The moment you want a user to provide markup that survives rendering (rich-text editors, markdown previews, embedded HTML email), entity escaping is exactly the wrong answer because it makes everything literal. For that case you need an allowlist-based sanitiser like DOMPurify, which parses the HTML, drops dangerous elements (<script>, <iframe>), strips dangerous attributes (onerror, javascript: URLs), and serialises back. The decision tree above is the cheat sheet: text content uses this snippet, rich content uses DOMPurify, and URL attributes always need a separate URL-scheme allowlist. Do not write your own HTML sanitiser; the parser quirks alone are a multi-year project.
