When you build HTML as a string, some characters in it are instructions to the browser, not text: < starts a tag, and & starts a character reference. A character reference is a short code that stands for one character, such as < for < and & for &; they're often called HTML entities. The browser shows the character a reference stands for as text, so < appears on the page as < but never starts a tag. HTML escaping means replacing the special characters in a piece of text with references, so the browser shows the text exactly as it was written.
It matters most when the text comes from someone else: a user's name, a comment, a search query, data from an API. Put into HTML as it is, any markup in it becomes part of your page, and markup can run JavaScript. That's called cross-site scripting (XSS): an attacker's code runs in your page with the same powers as your own, so it can read what the page shows and send requests as the signed-in user. Say a user sets their name to <img src=x onerror=alert(1)>, and the page greets them with "<p>Hello, " + name + "</p>":
| The HTML the page builds | What the browser does |
|---|---|
<p>Hello, <img src=x onerror=alert(1)></p> | Creates an <img> element. x isn't a real image, so loading it fails, and the browser runs the code in onerror. An attacker would put something worse than alert(1) there. |
<p>Hello, <img src=x onerror=alert(1)></p> | The name was escaped. The page shows the text Hello, <img src=x onerror=alert(1)>, as the user typed it, and nothing runs. |
A <script> element in the name would run too, when the HTML is built on the server and sent as the page. Scripts added with innerHTML don't run, which is why attacks there use event attributes such as onerror instead.
Which characters matter depends on where the text goes:
| Character | Where it matters | What it does there |
|---|---|---|
< | In an element's content | Starts a tag, such as <img, or an end tag, such as </p>. |
& | In content and in attribute values | Starts a character reference, so a user who typed < would see < instead. |
" | In an attribute value wrapped in " | Ends the value early, and the rest becomes new attributes: a title of x" onmouseover="alert(1) adds an onmouseover handler. |
' | In an attribute value wrapped in ' | Ends the value early, in the same way. |
> | Nowhere in content or in quoted attribute values | It only ends a tag that's already open. Escaping it anyway is the convention, and it does no harm. |
Escaping protects text in an element's content and in a quoted attribute value. Other places, such as a URL in an href or code inside a <script>, need different protection. Day to day, you often don't escape by hand: setting textContent puts text into the page as text, and React and Vue treat values you put in with {name} and {{ name }} as text, unless you opt out with dangerouslySetInnerHTML or v-html. You need escaping when you build an HTML string yourself, for innerHTML or in a server-side template.
Write escapeHtml(value). It returns value as a string that's safe to put inside HTML, as element content or inside a quoted attribute value.
Replace each of these five characters with its character reference: & with &, < with <, > with >, " with ", and ' with '. Replace every occurrence, each exactly once, and leave every other character as it is. Text that already looks escaped is escaped again: < becomes &lt;.
If value isn't a string, convert it with String first.
escapeHtml('<a href="x">Tom & Jerry</a>');
// '<a href="x">Tom & Jerry</a>'
escapeHtml("It's 5 > 3"); // "It's 5 > 3"
escapeHtml(42); // "42"
expect(escapeHtml("<b>Tom & Jerry</b>")).toBe("<b>Tom & Jerry</b>");
expect(escapeHtml('"it\'s"')).toBe(""it's"");
expect(escapeHtml('<a title="x">')).toBe("<a title="x">");
expect(escapeHtml("a<b<c>>d&&e")).toBe("a<b<c>>d&&e");
expect(escapeHtml("<")).toBe("<");
expect(escapeHtml('"&"')).toBe(""&"");
expect(escapeHtml("1 > 0")).toBe("1 > 0");
expect(escapeHtml("<b> &")).toBe("&lt;b&gt; &amp;");
expect(escapeHtml("Café / 50% off = {deal} #1")).toBe("Café / 50% off = {deal} #1");
expect(escapeHtml("")).toBe("");
expect(escapeHtml(42)).toBe("42");
expect(escapeHtml(null)).toBe("null");
expect(escapeHtml({ toString() { return "<i>"; } })).toBe("<i>");
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Escape HTML. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.