XSS in React, Vue and Server Components

By Davy Rogers

Frameworks auto-encode by default. Here's where XSS still slips through.

React, Vue, Angular, Next.js - they auto-encode output by default. XSS is dramatically reduced. Not eliminated.

Where frameworks don't protect you

dangerouslySetInnerHTML / v-html

<div dangerouslySetInnerHTML={{ __html: userContent }} />
<div v-html="userContent"></div>

If userContent is untrusted, attackers inject scripts.

Need raw HTML? Sanitise first:

import DOMPurify from "dompurify";
const clean = DOMPurify.sanitize(userContent);

href/src with user URLs

<a href={userUrl}>Click here</a>

If userUrl is javascript:alert(document.cookie), clicking executes JS.

Fix:

function isSafeUrl(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ["http:", "https:", "mailto:"].includes(parsed.protocol);
  } catch {
    return false;
  }
}

Server Components

If server data includes unsanitised HTML that the client renders with dangerouslySetInnerHTML, XSS happens. Sanitise before rendering raw HTML, regardless of source.

Third-party components

Review anything that accepts HTML, rich text, or markdown. A poorly written component might use innerHTML internally.

Template injection via SSR

// Next.js API route - bypasses React encoding
export default function handler(req, res) {
  const name = req.query.name;
  res.send(`<html><body>Hello ${name}</body></html>`);
}

Don't construct HTML strings manually.

postMessage

// Vulnerable
window.addEventListener("message", (event) => {
  document.getElementById("output").innerHTML = event.data;
});

// Fixed
window.addEventListener("message", (event) => {
  if (event.origin !== "https://trusted.example.com") return;
  document.getElementById("output").textContent = event.data;
});

Content Security Policy

Defence in depth. Even if XSS payload makes it in, CSP can block execution.

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

Frameworks needing inline script for hydration use nonce-based CSP:

Content-Security-Policy: script-src 'self' 'nonce-abc123'

Markdown rendering

Many renderers allow raw HTML by default:

This is a comment: <img src=x onerror=alert(1)>

Fix:

import { marked } from "marked";
import DOMPurify from "dompurify";
const html = DOMPurify.sanitize(marked.parse(userMarkdown));

Checklist

  • No dangerouslySetInnerHTML, v-html, innerHTML with unsanitised data
  • Validate URL schemes before href/src
  • textContent not innerHTML for dynamic text
  • Sanitise markdown output
  • Validate postMessage origins
  • Deploy CSP
  • Review third-party components
  • Don't construct HTML strings in SSR

The takeaway

The escape hatches - dangerouslySetInnerHTML, v-html, unvalidated URLs - are where mistakes happen. Sanitise with DOMPurify. Validate URLs. Deploy CSP.

Want a professional to look at it?Get an AppSec Health Check.