Every major frontend framework in production today escapes interpolated output by default — and every one of them also ships a single, clearly named function that turns that protection back off. React's JSX compiles {userInput} to text, not markup, so a string like <img src=x onerror=alert(1)> renders as literal characters on the page rather than executing. Vue's mustache syntax, {{ }}, does the same. Angular goes further at the framework level: its DomSanitizer runs automatically even when you bind to [innerHTML], stripping <script> tags and dangerous attributes before the DOM ever sees them. That's a meaningfully different default posture than React or Vue, both of which escape text interpolation but do nothing to sanitize HTML you deliberately ask them to render raw. The three escape hatches — dangerouslySetInnerHTML, v-html, and bypassSecurityTrustHtml() — exist for legitimate reasons (rendering CMS content, markdown output, rich-text editor state), but each is a single grep-able string that should trigger a security review every time it appears in a diff. This post compares the defaults, the escape hatches, and what actually catches misuse before it ships.
Why does JSX block XSS by default in React?
JSX blocks XSS by default because React never parses interpolated {} expressions as HTML — it converts them to DOM text nodes via createTextNode-equivalent rendering, so angle brackets and quotes are rendered literally rather than parsed as tags. A component that renders <div>{comment.body}</div> is safe even if comment.body contains a full <script> payload, because the string is inserted as text content, not markup. The only path around this is the aptly named dangerouslySetInnerHTML={{ __html: value }} prop, which maps directly to the DOM's innerHTML setter and renders whatever markup it's given, scripts included, if the value came from an untrusted source. React's own documentation flags the prop as dangerous by name rather than quietly supporting raw HTML through a normal prop, and community security write-ups — including guidance from Philippe De Ryck's pragmaticwebsecurity.com and repeated LogRocket and DEV Community posts on the topic — converge on the same recommendation: never pass unsanitized input to it, and run a library like DOMPurify over the string first if raw HTML rendering is unavoidable.
Why does Vue treat v-html as an explicit warning, not just a feature?
Vue treats v-html as an explicit warning because its own official documentation states, in the directive's description, that dynamically rendering arbitrary HTML on your site can lead to XSS vulnerabilities and that v-html should only be used on content you trust, never on user-provided content. Vue's default template interpolation — {{ message }} — is compiled to text-binding calls that escape HTML entities automatically, so {{ comment }} displaying <b>hi</b> shows the literal tags on the page rather than bolding the word. v-html, by contrast, compiles to a direct innerHTML assignment on the bound element, with no sanitization step in between. Framework maintainers put the warning directly in the directive's docs entry rather than only in a separate security guide, which matters in practice: a developer skimming the Vue API reference for "how do I render HTML from a string" lands on the warning in the same breath as the feature, not several pages away in a security appendix most people never open.
What makes Angular's default handling of [innerHTML] different?
What makes Angular different is that binding to [innerHTML] does not give you raw HTML rendering at all unless you separately opt out of sanitization — Angular's built-in DomSanitizer service intercepts values bound to security-sensitive contexts (HTML, styles, URLs, resource URLs) and strips or neutralizes dangerous content automatically, every time, with no extra step required from the developer. A <div [innerHTML]="userContent"></div> binding in Angular will silently drop a <script> tag or an onerror handler even if the developer never thought about sanitization. The actual escape hatch is calling DomSanitizer.bypassSecurityTrustHtml() (or its sibling methods for URLs, styles, and resource URLs), which wraps a value in a marker telling Angular's rendering pipeline to skip sanitization for that specific value. Because bypassSecurityTrust* calls have to be made explicitly, in application code, security reviewers and static analysis tools treat every occurrence as a point requiring justification — it's one of the few places in Angular where a single method name reliably indicates a developer has taken on responsibility the framework would otherwise carry.
Why do all three frameworks still allow raw HTML rendering at all?
All three frameworks still allow raw HTML rendering because legitimate applications genuinely need it — a CMS-driven marketing page, a markdown-to-HTML preview pane, or a rich-text editor's saved output all require inserting real markup, not escaped text, somewhere in the render tree. Blocking that entirely would push every team needing it toward manual document.body.innerHTML manipulation outside the framework's rendering lifecycle, which is worse: it bypasses the framework's virtual DOM diffing, loses whatever lifecycle hooks or sanitization the framework does apply elsewhere, and is far less visible in code review than a named prop or directive. By keeping the unsafe path inside the framework's own API — dangerouslySetInnerHTML, v-html, bypassSecurityTrustHtml() — each framework keeps the operation searchable, lintable, and reviewable as a single, consistent pattern instead of dozens of ad hoc DOM manipulations scattered through a codebase.
How should teams actually catch misuse of these escape hatches before it ships?
Teams catch misuse most reliably by combining a sanitization library at the point of use with static analysis that flags every occurrence of the escape hatch in CI, rather than relying on code review alone to remember three different framework-specific patterns. DOMPurify is the sanitizer most commonly recommended across React, Vue, and Angular guidance alike for cases where raw HTML truly is necessary, because it's actively maintained and specifically designed to strip executable content while preserving safe markup. On the detection side, source-to-sink dataflow analysis is what separates a dangerouslySetInnerHTML call fed a hardcoded string (safe) from one fed a value that traces back to a URL parameter or an API response (a real finding). Safeguard's SAST engine, part of its unified application security testing pipeline, performs exactly this kind of source-to-sink tracing for JavaScript and TypeScript, mapping findings to CWE and OWASP categories with the dataflow trace attached — so a bypassSecurityTrustHtml() call fed unsanitized input surfaces as a scoped, explainable finding in a pull request rather than something a reviewer has to notice by eye.