How to Prevent DOM-Based XSS With Entity Encoding: Hex vs Named Entities
The hex-versus-named question is mostly a myth
Ask whether hexadecimal entities are safer than named entities for preventing XSS and you will get a confident answer, usually in favor of hex. The answer is wrong, and the reason it matters is that it sends people to fix the wrong thing.
To an HTML parser, <, < and < are the same token. All three are resolved to U+003C, the less-than sign, before any script can run. There is no code path in which a named entity is decoded more permissively than a numeric one, and no payload that survives one form and is neutralized by the other. Choose a form because it is supported where the string will be consumed, not because one is believed to be more secure.
| Character | Named | Decimal | Hex |
|---|---|---|---|
& | & | & | & |
< | < | < | < |
> | > | > | > |
" | " | " | " |
' | none in HTML 4 | ' | ' |
The one genuine asymmetry in that table is the last row. HTML 4 defined no named entity for the apostrophe, which is why ' is an XML addition that older HTML parsers do not recognize. A tool that emits ' into a document processed by an HTML 4 parser leaves an apostrophe unencoded, and an unencoded apostrophe is enough to break out of a single-quoted attribute. That is a real defect — but it is a coverage defect, not evidence that numeric forms are stronger. The correct fix is to emit ' or ' for the apostrophe, which is exactly what a well-built encoder does, and what this one does.
What actually goes wrong: the escape set, not the numeral system
Entity encoding fails for four reasons, and none of them is hexadecimal.
The first is an incomplete set. Ampersand, less-than, greater-than, both quote characters — omit any one and there is a context in which the payload escapes. Omitting the ampersand is the most damaging, because it lets an attacker write an entity of their own choosing: input the literal text <script> into a field that encodes angle brackets but not ampersands, and the browser decodes it to a real script tag.
The second is the wrong context. Entity encoding is HTML escaping. It does nothing for a value placed in a URL, where the correct treatment is percent-encoding, and nothing for a value interpolated into a JavaScript string, where the correct treatment is JavaScript escaping. A javascript: URL is the canonical example: javascript:alert(1) contains no characters that HTML entity encoding changes, so a filter built only on entities sees nothing to do and passes the payload through intact.
The third is encoding twice. If a value is encoded once and then the already-encoded output is encoded again, the user sees &lt; on the page rather than <, and the escaping has become a display bug. The mirror image is decoding twice, which is more dangerous because it manufactures a payload the encoder never saw. Take the string &#60;script> — a single decode yields <script>, still inert as text; a decoder that rescans its own output resolves both layers and produces a live tag. Decoding must be a single left-to-right pass with no rescanning, and any implementation that chains a hex pass, a decimal pass and a named pass over the same string has this bug by construction.
The fourth is using innerHTML in the first place. More on that below.
Why a developer tool is a harder case than a blog
A marketing site renders text a human wrote. A developer tool is built to accept arbitrary input and display it in several different ways, sometimes as text and sometimes as rendered output. That dual behavior is the vulnerability surface. A JSON formatter, a markdown preview, a regex visualizer and a code playground all decode, display and sometimes re-render untrusted strings, and each of those steps is a place where the escape set can be applied in the wrong context or skipped entirely.
The practical distinction to hold on to is between output that will be shown as text and output that will be shown as markup:
- Shown as text — use
textContent. The browser does no parsing, so no encoding is required and no encoding can be forgotten. - Shown as markup you constructed — build the string from trusted pieces and place only encoded values inside it.
- Shown as markup the user supplied — that is a sanitizer's job, not an encoder's. An allow-list library decides which tags and attributes survive; entity encoding cannot, because its whole purpose is to make supplied markup inert.
The third case is where encoders get over-trusted. A markdown renderer is asked to turn user text into HTML, so the user's angle brackets have to become real tags somewhere. Encoding the input makes the renderer useless; not encoding it makes the page vulnerable. The resolution is that the renderer emits a restricted tag set of its own, with every attribute value encoded, rather than passing anything through from the source.
A test set that separates a working encoder from a decorative one
- An apostrophe: confirm the output is
', not a bare'and not an unsupported named entity. - A literal entity: input the six characters of
<and confirm the ampersand is encoded, so the output displays as that text rather than turning into a less-than sign. - A double-encoded entity: input
&#60;script>and confirm that decoding it once produces<script>and not a tag. - An attribute break-out attempt: input
" onmouseover="alert(1)into a quoted attribute and confirm every quote in the output is encoded. - A URL context: confirm the tool does not claim to protect a value destined for an
href, because entity encoding will not stop ajavascript:scheme. - An astral character: input an emoji and confirm a single entity is produced rather than a pair of surrogate-range entities.
The ordering that actually prevents DOM-based XSS
Prevention comes from a sequence, and entity encoding is only one step in it. Prefer APIs that cannot execute — textContent, setAttribute, createTextNode — over innerHTML and insertAdjacentHTML. When markup is unavoidable, encode with a complete set covering ampersand, angle brackets and both quote characters, once. Sanitize with an allow-list when the input is genuinely meant to be markup. Apply URL and JavaScript escaping for those contexts instead of HTML escaping. And validate your choice of <form> target: a tool that has no form and no server round-trip still needs the first three steps, because DOM-based XSS never touches the server at all.
The hex-versus-named debate comes last and matters least. Pick the form the receiving parser undeniably supports, make sure the apostrophe and the ampersand are in the set, and spend the remaining effort on which sink the value is written to.
Frequently Asked Questions
What is DOM-based XSS and how does client-side encoding prevent it?
DOM-based XSS occurs when client-side scripts execute untrusted data within the Document Object Model. Client-side encoding prevents this by neutralizing special characters before they are interpreted as code, ensuring the browser renders them safely as text.
Should I use hex entity encoding or named HTML entities to prevent XSS?
It makes no security difference. To an HTML parser the named, decimal and hexadecimal forms of the same character are identical - each resolves to the same code point before any script can run. What matters is that the escape set is complete (ampersand, both angle brackets, both quote characters), that it matches the context, and that it is applied exactly once. The single genuine asymmetry is the apostrophe, which has no named entity in HTML 4, so a numeric form such as the decimal 39 entity should be emitted instead of the XML named form.
Does HTML entity encoding stop DOM-based XSS completely?
HTML entity encoding is highly effective at stopping DOM-based XSS when inserting untrusted data into HTML body or attribute contexts, but it is not a silver bullet. You must use context-specific encoding, meaning you cannot rely on HTML entities to protect against JavaScript context injections or URL-based DOM sinks.
How do I implement hex entity encoding in JavaScript?
You can implement hex entity encoding in JavaScript by creating a function that replaces dangerous characters like <, >, and " with their respective hex equivalents (e.g., replacing < with <). Many modern developer tools and security libraries provide built-in functions to handle this conversion automatically without requiring manual string manipulation.
Are named HTML entities less secure than hex entities for XSS protection?
No. A named entity that the receiving parser understands is exactly as safe as its numeric equivalent. The real weakness is coverage, not the numeral system: named entity tables are finite, so a character with no name is left unencoded and can break out of an attribute. Numeric references cover every code point, which makes them the safer default for a tool that has to encode arbitrary input - not because numbers are stronger, but because the mapping is exhaustive.
Can I use named entities like < and > in the DOM safely?
Yes, using standard named entities like < and > is safe and widely supported for encoding content within HTML body elements. However, relying strictly on hex encoding ensures consistency across all contexts, especially when dealing with edge cases in older browsers or complex attribute structures.
What is the difference between HTML entity encoding and URL encoding for DOM XSS?
HTML entity encoding is used when injecting untrusted data into HTML elements or attributes, while URL encoding is required when inserting data into URL parameters or src attributes. Using the wrong type of encoding for the specific DOM context is a leading cause of XSS vulnerabilities in web applications.
Do I need client-side encoding if I already sanitize input on the server?
Yes, client-side encoding is still necessary because DOM-based XSS can occur entirely within the browser without ever hitting the server. If your JavaScript reads data from a URL fragment or local storage and writes it to the DOM, server-side sanitization will not protect you from the vulnerability.
What are the best developer tools for automating DOM XSS encoding?
Popular frontend libraries like DOMPurify, OWASP ESAPI, and framework-specific tools like Angular's built-in sanitization automate DOM XSS encoding safely. These tools automatically detect the correct context and apply the appropriate hex entity encoding or other sanitization methods, reducing the risk of human error.
How does hex entity encoding work inside HTML attributes?
When placing untrusted data into HTML attributes, hex entity encoding converts characters like quotes (") into " to prevent the attacker from breaking out of the attribute. This ensures the browser treats the injected data as a literal string value rather than executable markup or script.