Modern frameworks like React, Vue, Angular, and Next.js auto-encode output by default, which has dramatically reduced XSS. But "reduced" is not "eliminated." This lesson covers where XSS still happens in component-based frameworks and what to do about it.
Why frameworks help
Traditional server-rendered pages concatenated HTML strings. One missed encoding and you had XSS. Modern frameworks changed the game:
- React escapes all values embedded in JSX by default.
<p>{userInput}</p>renders as text, not HTML. - Vue escapes template interpolations
{{ userInput }}the same way. - Angular sanitises bound values and strips unsafe HTML from bindings automatically.
- Next.js / Nuxt / SvelteKit inherit the protections of their underlying framework.
If you use the framework as intended, you get XSS protection for free in most cases.
Where frameworks do not protect you
dangerouslySetInnerHTML (React) / v-html (Vue) / innerHTML binding (Angular)
Every framework provides an escape hatch for rendering raw HTML. The names are deliberately scary:
// React — XSS risk if content is untrusted
<div dangerouslySetInnerHTML={{ __html: userContent }} />
<!-- Vue — XSS risk if content is untrusted -->
<div v-html="userContent"></div>
If userContent comes from user input, an attacker can inject script tags, event handlers, or any other HTML.
When you need raw HTML (e.g., rendering markdown or CMS content), sanitise it first:
import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(userContent);
DOMPurify is the gold standard for HTML sanitisation. It strips dangerous elements and attributes while preserving safe formatting.
href and src attributes with user-controlled URLs
JSX auto-encoding does not protect against javascript: URLs:
// React does NOT block this
<a href={userUrl}>Click here</a>
If userUrl is javascript:alert(document.cookie), clicking the link executes JavaScript. React escapes HTML entities but does not validate URL schemes.
Fix: Validate URL schemes before rendering.
function isSafeUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
return ["http:", "https:", "mailto:"].includes(parsed.protocol);
} catch {
return false;
}
}
Server components and RSC payloads
React Server Components (RSC) and similar patterns in Next.js 13+ introduce new serialisation boundaries. Data flows from server to client via a serialised payload. If server data includes unsanitised HTML that the client renders with dangerouslySetInnerHTML, the XSS happens on the client even though the data originated on the server.
The rule is the same: sanitise before rendering raw HTML, regardless of where the data came from.
Third-party component libraries
You trust your own code, but what about the component library you installed? A poorly written third-party component might use innerHTML internally. Review components that accept HTML content, rich text, or markdown.
Template injection via SSR
Server-side rendering (SSR) in frameworks like Next.js, Nuxt, or SvelteKit generates HTML on the server. If your SSR logic constructs HTML outside the framework's template system (e.g., string interpolation in a getServerSideProps response), you bypass the framework's auto-encoding.
// Next.js API route returning HTML — bypasses React's encoding
export default function handler(req, res) {
const name = req.query.name;
res.send(`<html><body>Hello ${name}</body></html>`);
// XSS if name contains <script>
}
Use the framework's rendering pipeline. Do not construct HTML strings manually on the server.
postMessage and cross-origin communication
Single-page applications often use window.postMessage() for cross-origin or cross-frame communication. If the message handler does not validate the origin and uses the message data in DOM operations:
// Vulnerable — no origin check, uses innerHTML
window.addEventListener("message", (event) => {
document.getElementById("output").innerHTML = event.data;
});
Fix: Always check event.origin and never use innerHTML with message data.
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.example.com") return;
document.getElementById("output").textContent = event.data;
});
Content Security Policy (CSP)
CSP is a defence-in-depth measure. Even if an XSS payload makes it into the page, CSP can prevent it from executing.
A good starting point:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'
Key directives:
script-src 'self'— only allow scripts from your own origin. No inline scripts, noeval().object-src 'none'— block Flash, Java applets, and other plugin-based content.base-uri 'self'— prevent<base>tag injection that redirects relative URLs.
For frameworks that need inline script for hydration (Next.js, Nuxt), use nonce-based CSP:
Content-Security-Policy: script-src 'self' 'nonce-abc123'
The framework generates a unique nonce per request and applies it to its script tags. Attacker-injected scripts will not have the nonce and will be blocked.
XSS in markdown rendering
If your application renders user-supplied markdown (comments, wiki pages, documentation), the markdown-to-HTML conversion can introduce XSS. Many markdown renderers allow raw HTML by default.
This is a comment with a surprise: <img src=x onerror=alert(1)>
Fix: Disable raw HTML in the markdown renderer, or sanitise the output:
import { marked } from "marked";
import DOMPurify from "dompurify";
const html = DOMPurify.sanitize(marked.parse(userMarkdown));
Checklist for framework-based applications
- Never use
dangerouslySetInnerHTML,v-html, orinnerHTMLwith unsanitised data - Validate URL schemes before rendering in
hreforsrcattributes - Use
textContent(notinnerHTML) for dynamic text in vanilla JS - Sanitise HTML output from markdown renderers
- Validate
postMessageorigins and avoid DOM manipulation with message data - Deploy CSP headers with
script-srcrestrictions - Review third-party components that accept HTML or rich text
- Do not construct HTML strings manually in SSR code
Summary
Modern frameworks have made XSS harder to introduce, but they have not eliminated it. The escape hatches (dangerouslySetInnerHTML, v-html, unvalidated URLs) are where mistakes happen. Sanitise any raw HTML with DOMPurify, validate URL schemes, deploy CSP as a safety net, and never bypass the framework's rendering pipeline to construct HTML strings manually.
