Escape HTML Strings: Prevent XSS Attacks in Frontend

The Dangerous Shortcut You're Probably Taking Right Now

Picture this: you're building a feature that displays user-generated comments on your frontend. The deadline is looming. So you grab the input, maybe run a quick <code>.replace(/</g, '<') on it, and inject it into the DOM via innerHTML. Done. Ship it.

Here's the problem — that single regex replacement misses **at least 5 other critical characters** that attackers exploit daily. According to OWASP, cross-site scripting (XSS) accounts for roughly **17%** of all web application vulnerabilities logged in recent years, and the majority of those breaches trace back to incomplete HTML escaping in frontend code.

The hard truth? Most developers know they *should* escape HTML strings to prevent XSS attacks. Far fewer know how to do it correctly, completely, and without breaking legitimate content. Let's walk through the common mistakes and build a proper escaping strategy step by step.

Misconception #1: "Replacing < and > Is Enough"

This is probably the most widespread mistake in frontend development. Developers assume that if angle brackets are neutralized, attackers can't inject tags, so the threat is gone.

Why This Fails

Consider this payload that doesn't use a single angle bracket:

" onmouseover="alert(document.cookie)" autofocus="

If this string lands inside an HTML attribute — say, <input value="USER_DATA"> — and you only escaped < and >, the attacker just hijacked the element. The browser sees a valid onmouseover handler and executes JavaScript the moment the user's cursor drifts over the input.

The Correction: Escape All OWASP-Recommended Characters

To properly escape HTML strings and prevent XSS, you need to handle **six** characters minimum. Here's the complete mapping that OWASP recommends for HTML context:

  • & → &amp;
  • < → &lt;
  • > → &gt;
  • " → &quot;
  • ' → &#x27;
  • / → &#x2F;

The forward slash matters because it can close out tags prematurely. The ampersand must be escaped **first** — otherwise, you'll double-encode your other replacements. Order of operations is not optional here.

Here's a correct implementation in JavaScript:

function escapeHtml(str) {
  if (typeof str !== 'string') return '';
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;');
}

Notice the sequence. Ampersand comes first. Always. If you escape < into &lt; and *then* escape ampersands, you'll turn &lt; into &amp;lt;, which renders as literal text instead of a less-than symbol. Your UI breaks, and users see raw entity codes floating around the page.

Misconception #2: "innerText and innerHTML Are Interchangeable"

Another classic. Developers treat innerText and innerHTML as two ways to do the same thing, picking whichever feels convenient.

Why This Fails

They behave fundamentally differently. innerHTML parses the string as HTML markup, which means any tags, event handlers, or script elements in the string get rendered as live DOM nodes. innerText and textContent, by contrast, treat the string as plain text — the browser will display angle brackets on screen without interpreting them.

If you're dynamically inserting untrusted data and you use innerHTML without escaping, you've opened the door to script injection. If you use textContent, the browser handles the escaping for you.

The Correction: Choose the Right Property for the Context

Ask yourself one question: **does this content need to be rendered as HTML?**

If the answer is no — and it usually is — use textContent:

// Safe: browser treats this as plain text
element.textContent = userInput;

If the answer is yes (you need formatting, links, or structured content), then you **must** escape the string first, or better yet, use a sanitization library. Don't reach for innerHTML with raw user data and hope for the best.

Misconception #3: "A Single Escape Function Works Everywhere"

Many frontend developers write one escapeHtml function and call it a day, applying it everywhere — inside HTML body content, inside attributes, inside JavaScript event handlers, inside URLs.

Why This Fails

XSS prevention is **context-sensitive**. The escaping rules differ depending on where the untrusted data lands in the document. A string that's safe inside a <div> body might be dangerous inside a <script> block or a href attribute.

Consider this template:

<a href="javascript:USER_INPUT">Click here</a>

Even if you escape angle brackets, an attacker can input alert(1) and the browser will execute it because the javascript: protocol is live. HTML escaping alone doesn't protect you here — you need URL context validation.

The Correction: Escape Based on Output Context

Step through each context your data will enter:

1. HTML Body Context — Use the full six-character escape function shown above. This covers text between tags like <p>, <div>, <span>.

2. HTML Attribute Context — Same escape function works, but you must also ensure the attribute is quoted. Never place untrusted data in an unquoted attribute. The character space becomes an attack vector in unquoted attributes because it can inject new attributes.

3. URL Context — Before placing user data in a href or src, validate the protocol. Whitelist http:, https:, and relative URLs. Reject javascript:, data:, and vbscript: outright. Then URL-encode the remaining string with encodeURIComponent().

4. JavaScript Context — If user data must land inside a <script> tag or an event handler, use JSON.stringify() to safely encode it, and place it inside a quoted string. Never inject raw data directly into JavaScript code.

Misconception #4: "Escaping Alone Is Sufficient for Rich Content"

Sometimes you genuinely need to render user-provided HTML — a rich text editor, a markdown parser, a comment system with basic formatting. Developers often try to hand-roll an escaping solution that allows some tags but blocks others.

Why This Fails

Allowlist-based filtering is notoriously difficult to get right. Attackers are creative. They use nested tags, encoded characters, Unicode tricks, and obscure attributes to slip past naive filters. A study by Google's security team found that even experienced developers miss edge cases when building custom HTML sanitizers — the attack surface is simply too large.

Consider this seemingly harmless input:

<img src="x" onerror="stealSessionToken()">

If your filter allows <img> tags but doesn't strip event handlers, you've just handed attackers an execution vector.

The Correction: Use a Battle-Tested Sanitization Library

When you need to render rich HTML content, stop trying to build your own filter. Reach for **DOMPurify** — a widely adopted, actively maintained library that's been audited and tested against thousands of XSS payloads.

Here's how to use it:

import DOMPurify from 'dompurify';

const cleanHtml = DOMPurify.sanitize(userInput, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title']
});

element.innerHTML = cleanHtml;

DOMPurify parses the HTML, strips dangerous elements and attributes, and returns a safe string you can inject via innerHTML. It handles context-aware sanitization, mutation XSS, and edge cases that manual escaping will miss.

For frameworks like React, Vue, and Angular, the good news is that they **auto-escape** by default. React's JSX, Vue's mustache syntax {{ }}, and Angular's interpolation all escape strings automatically. XSS typically sneaks in when developers explicitly bypass these protections using dangerouslySetInnerHTML, v-html, or Angular's [innerHTML] binding with raw data. That's the moment you need DOMPurify.

Building a Defense-in-Depth Strategy

Escaping HTML strings is your first line of defense, but it shouldn't be your only one. A robust frontend security strategy layers multiple protections:

Output escaping — Every piece of untrusted data gets escaped for its specific output context. No exceptions, no shortcuts.

Content Security Policy (CSP) — Add a CSP header that restricts script sources. Even if an XSS payload slips through, a strict CSP can block inline script execution. A policy like script-src 'self' eliminates most injection-based attacks.

Cookie flags — Set HttpOnly on session cookies so JavaScript can't access them. If an XSS attack does fire, the attacker can't trivially steal session tokens.

Input validation — Validate input length, format, and type on the server side. This isn't XSS prevention directly, but it reduces the attack surface and catches malformed data early.

The Bottom Line

Escaping HTML strings to prevent XSS attacks isn't a one-liner you tack onto the end of a function. It's a context-aware practice that demands you understand where your data lands, what characters matter, and which tools to trust. Stop relying on partial regex replacements. Stop treating innerHTML and textContent as interchangeable. Stop assuming one escape function covers every scenario.

Escape all six OWASP-recommended characters, in the right order, for the right context. Use textContent when you don't need markup. Use DOMPurify when you do. Layer CSP and cookie protections underneath. That's how you build frontend code that stays standing when attackers come knocking.

Frequently Asked Questions

How to escape HTML in JavaScript?

You can escape HTML in JavaScript by creating a text node using `document.createTextNode()` or by replacing special characters like `<`, `>`, and `&` with their corresponding HTML entities. Using a dedicated function to replace these characters ensures that the browser interprets them as plain text rather than executable code.

How to prevent XSS attacks in frontend development?

To prevent Cross-Site Scripting (XSS) attacks, never insert untrusted data directly into the DOM using properties like `innerHTML`. Always escape user input before rendering it, and rely on modern frameworks like React or Angular that automatically escape values by default.

What is the difference between escaping and sanitizing HTML?

Escaping HTML converts special characters into HTML entities, rendering the input as harmless plain text without executing any code. Sanitizing HTML, on the other hand, removes malicious scripts while allowing safe HTML tags to render normally, typically using a library like DOMPurify.

How do I escape HTML in React?

React automatically escapes strings embedded in JSX before rendering them, providing built-in protection against XSS attacks. If you absolutely must render raw HTML, you use the `dangerouslySetInnerHTML` attribute, but you should always sanitize the input first with a robust library.

Does using innerHTML cause XSS vulnerabilities?

Yes, using `innerHTML` to insert untrusted data directly into the DOM is a major cause of XSS vulnerabilities because the browser will parse and execute any HTML tags or JavaScript included in the string. To stay safe, use `textContent` instead, or ensure the data is thoroughly escaped or sanitized beforehand.

What is the best library to sanitize HTML in JavaScript?

DOMPurify is widely considered the best and most robust library for sanitizing HTML in frontend development. It strips out dangerous scripts and malicious code while safely rendering permitted HTML elements, making it highly effective for preventing XSS attacks.

How to encode HTML entities in JavaScript?

You can encode HTML entities by using a custom function that replaces characters like `<`, `>`, `"`, `'`, and `&` with their respective entity strings (e.g., `&lt;`, `&gt;`). Alternatively, you can leverage the browser's native encoding by assigning the string to a temporary element's `textContent` property and then reading its `innerHTML`.

How to escape HTML in jQuery?

In jQuery, you can escape HTML by creating a new element and using the `text()` method to set the content, which automatically encodes the string. Instead of using `$('#element').html(userInput)`, you should always use `$('#element').text(userInput)` to safely display untrusted data.

Why do I need to escape special characters in HTML?

Escaping special characters prevents the browser from interpreting user input as active HTML markup or JavaScript code. This is a critical security measure because it neutralizes injected scripts, stopping attackers from stealing session cookies or compromising user data.