Toolcedar

The Five Characters That Break HTML

Only five characters need escaping in HTML, one of them must be escaped first, and the context decides what escaping even means.

· 3 min read

HTML has no way to tell markup apart from content except by the characters used to write it. A less-than sign always looks like the start of a tag, whether you meant it as one or as a mathematical comparison. Escaping is how you say "this is content" — you replace the character with a named or numeric reference, and the browser renders the original symbol without acting on it.

The five, and the order they go in

  • & becomes & — the ampersand starts every entity, so it has to be handled first.
  • < becomes &lt; — otherwise the browser starts reading a tag.
  • > becomes &gt; — less dangerous alone, but escaped for symmetry and to survive sloppy parsers.
  • " becomes &quot; — needed inside any double-quoted attribute value.
  • ' becomes &#39; — needed inside any single-quoted attribute value.

The ordering is not stylistic. If you escape the less-than sign before the ampersand, the ampersand you just wrote in &lt; gets escaped in turn and the reader sees the literal text &amp;lt; on the page. Every correct escaper handles the ampersand first, and every double-encoding bug you will ever chase comes from something that did not.

Context decides the rules

Escaping is not one operation applied everywhere. The same value needs different treatment depending on where in the document it lands, and this is where most real vulnerabilities live rather than in the escaping itself.

In ordinary text content, the five above are enough. Inside an attribute value, quotes matter more than anything else, and an unquoted attribute is unsafe no matter what you escape, because a space alone ends it. Inside a URL attribute such as href or src, HTML escaping does nothing about a javascript: scheme. Inside a script element the rules change entirely, because the content is JavaScript and needs JavaScript escaping. Inside a style element they change again.

The question is never "is this escaped?" but "is this escaped for the place it is going?".

Why decoding is the dangerous direction

The shortcut everyone finds for decoding entities in the browser is to assign the string to an element's innerHTML and read the text back out. It works, and it is a bad habit: assigning to innerHTML parses the string as markup, which can execute event handlers as a side effect. That is an odd property for a routine that exists to handle untrusted input.

A decoder that walks the entity syntax and looks values up in a table has no such behaviour. It is slightly more code and it cannot be tricked into running anything.

Named, numeric and the ones that surprise people

Entities come in three forms: named such as &amp;, decimal such as &#38;, and hexadecimal such as &#x26;. All three produce the same character. Named entities are more readable but the list is long and inconsistently supported outside HTML — XML, for instance, defines only five of them, which is why &nbsp; is a common cause of an XML parse failure.

&amp;   &#38;   &#x26;    all render as &

&nbsp;  a space that never wraps and never collapses
&#8203; a zero-width space: invisible, and still a character

The non-breaking space deserves particular attention because it is invisible and behaves differently from the space it resembles. Text pasted from a word processor or a rich editor is frequently full of them, and they will not collapse, will not wrap, and will not match a search for a normal space. When a string comparison fails against text that looks identical, this is usually why.

Escaping is not sanitising

Escaping renders everything harmless by rendering it as text, which is the right default. Sanitising keeps some markup and removes the rest, which is what you need when users are allowed to submit formatted content. They are different jobs, and sanitising is genuinely hard — the allow-list has to cover elements, attributes, URL schemes and CSS. Escape by default, and reach for a maintained sanitiser only when markup must actually survive.

Frequently asked questions

Do I need to escape the greater-than sign?
Strictly it is only ambiguous in a few parsing states, so a lone greater-than sign usually renders fine. It is escaped anyway because the cost is nothing and it removes any dependence on how forgiving a particular parser happens to be.
Why does my page show &amp;lt; instead of a less-than sign?
The text was escaped twice. Something escaped the ampersand of an already-escaped entity, usually because a value passed through two layers that each tried to be safe. Decode once and check whether the result is correct before treating it as corrupt.
Is escaping enough to prevent cross-site scripting?
Only when the escaping matches the context. HTML escaping protects text content and quoted attributes, but does nothing inside a script block, inside a style block, or against a javascript: URL in an href. Each of those needs its own rule.