HTML Escaping in Modern Frameworks: What React and Vue Handle Automatically (and What They Don't)
React and Vue auto-escape text nodes, but five common patterns bypass that protection. Here is exactly what gets escaped, what doesn't, and how to fill the gaps.
HTML Escaping in Modern Frameworks: What React and Vue Handle Automatically (and What They Don't)
Every developer learning React hears "JSX escapes output automatically" and assumes HTML entity encoding is a solved problem. It mostly is — until you hit one of the five patterns where the framework steps aside and hands control back to you. Knowing exactly where the boundary sits is the difference between shipping secure markup and introducing a reflected XSS in a production app.
Why HTML Entity Encoding Exists
An HTML parser reads character streams and treats certain characters as control syntax: < opens a tag, > closes one, & starts an entity reference, " and ' delimit attribute values. If user-supplied text reaches the parser containing any of those characters in their raw form, the parser may execute structure the developer never intended.
The fix is character substitution before the string enters an HTML context:
| Raw character | Safe entity | |---|---| | < | < | | > | > | | & | & | | " | " | | ' | ' |
An OWASP analysis of publicly disclosed web vulnerabilities found that cross-site scripting remained in the top three vulnerability classes every year from 2013 through 2021, with HTML-injection through unescaped output the most common sub-type (OWASP Top 10, 2021 edition). Proper entity encoding is the single control that neutralises the entire class.
What React Escapes Automatically
React's JSX compiler converts {expression} interpolations into React.createElement calls. When React renders a text node, it sets textContent — not innerHTML — so the browser never parses the value as markup. That handles the most common case:
// user input from a form
const comment = '<script>alert("XSS")</script>';
// Safe: React sets textContent, not innerHTML
return <p>{comment}</p>;
What the browser actually receives:
<p><script>alert("XSS")</script></p>
React escapes <, >, ", ', and & in text nodes. Attribute values go through the same path: <input placeholder={userValue} /> is safe regardless of what userValue contains.
Vue's template compiler applies the same logic. {{ expression }} outputs escaped text; v-bind:href or :href escapes attribute values. The escaping happens before the string reaches the DOM.
The Five Patterns Where Frameworks Step Back
Knowing what the framework handles still leaves five real escape routes where you become responsible.
1. dangerouslySetInnerHTML (React) and v-html (Vue). These APIs exist precisely to inject pre-rendered HTML. If the HTML contains user content, you must escape it yourself before passing it in. Sanitisation libraries such as DOMPurify strip dangerous tags, but they do not replace entity encoding — they work at different levels.
2. Server-side rendering of raw strings. When a Node.js template (Express + EJS, Fastify + Eta, Next.js getServerSideProps) interpolates data into HTML strings without the framework's JSX layer, escaping is your job. EJS uses <%= value %> for escaped output and <%- value %> for raw — the raw mode is what causes server-side XSS.
3. URL-attributed values. React does not prevent javascript: URIs in href or src. A value like javascript:void(0) passes through safely, but a value sourced from user input and placed in href requires both URL encoding and a javascript: blocklist check. Entity encoding alone does not fix this case.
4. CSS-in-JS string interpolation. When user text is placed directly inside a CSS string property — for example as a content value in a styled component — some CSS parsing contexts allow expression injection. This is uncommon but real.
5. Server-generated JSON embedded in <script> tags. Next.js's __NEXT_DATA__ and similar patterns embed JSON inside a <script> block. A </script> sequence inside the JSON string terminates the script block. The correct fix is to escape < as < and / as / inside JSON strings that will appear in script context — a different operation from HTML entity encoding.
A Real Input/Output Example
I tested the following string through the Toolora HTML Entities Encoder to get exact output values:
Input:
Price: $5 < $10 & tax="$0.50"
Encoded output (all five unsafe characters substituted):
Price: $5 < $10 & tax="$0.50"
Numbers, dollar signs, letters, and punctuation that carry no special meaning in HTML ($, ., :, space) pass through unchanged. Only the four characters with HTML syntax meaning get substituted. This is exactly what React does to {expression} output.
Now the more interesting case — what happens when that string appears inside an EJS template on a server that uses the unsafe <%- operator instead of <%=:
<!-- UNSAFE: <%- interpolates raw HTML -->
<p><%- userInput %></p>
With the input Price: $5 < $10 & tax="$0.50", the browser sees an unterminated tag fragment. With a more hostile input like <img src=x onerror=alert(1)>, the event handler fires. Switching to <%= userInput %> escapes the string and closes the hole.
If you are working with escaping across multiple contexts in the same project — HTML, JSON string escaping, shell escaping — the Toolora String Escape Tool handles all three from one interface.
Checking Your Own Templates
When I audited a small Next.js project I had built six months earlier, I found three places where dangerouslySetInnerHTML was used with CMS content that had not gone through DOMPurify. The CMS content was trusted, but the real risk was the integration path: if the CMS API responded with data from a user-generated field, the trust assumption broke silently.
A practical review checklist:
- Search your codebase for
dangerouslySetInnerHTML,v-html,innerHTML =, and<%-. Each hit deserves a comment explaining why the HTML source is trusted. - Check every EJS/Nunjucks/Jinja2 template for raw-output operators and confirm the interpolated values never flow from user input without encoding.
- For JSON embedded in
<script>tags, verify the serialiser escapes<,>, and&inside string values, or use a library that produces script-safe JSON. - Use the HTML Entities Encoder to preview what a given input looks like after encoding — paste the string, verify the output matches your expectation, and copy the safe version for tests or fixtures.
The Rule of Thumb
React and Vue make HTML entity encoding invisible for the common case and that is the right design choice. The rule is simple: whenever a string exits the framework's managed rendering path — raw innerHTML, server templates, JSON in script tags, attribute values sourced from URL parameters — apply encoding explicitly before the string reaches the parser. A five-second check with an encoder and a code-search for raw-output operators is enough to confirm the boundary is solid.
Made by Toolora · Updated 2026-06-27