Stored XSS (OWASP A03:2021, CWE-79) in user-generated content is one of the most severe vulnerabilities a community platform can have. When unsanitized HTML or script tags are persisted to the database and rendered in other users' browsers, an attacker can steal session cookies, exfiltrate auth tokens, hijack accounts, or redirect victims to phishing pages — all without any interaction beyond viewing a post or comment. Every user who views the malicious content becomes a victim. Client-side sanitization alone is bypassed trivially via direct API calls; server-side sanitization before storage is the only reliable control.
Critical because stored XSS lets an attacker execute arbitrary JavaScript in every viewer's browser, enabling session hijacking and account takeover at scale.
Sanitize all user-generated content server-side before writing to the database. Use dompurify on the backend with a strict allowlist:
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM('');
const purify = DOMPurify(window);
export function sanitizeContent(html) {
return purify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
}
On the frontend, render content as text nodes — never via dangerouslySetInnerHTML — unless the content passed through your server-side sanitizer and you have explicit need for rich HTML. When in doubt, strip all markup.
ID: community-moderation-safety.content-filtering.xss-prevention
Severity: critical
What to look for: Check if user-submitted content is sanitized before storage and before display. Look for direct HTML rendering, innerHTML, or unsafe rendering patterns. Verify that content is escaped or sanitized with libraries like dompurify or sanitize-html.
Pass criteria: All user-generated content is sanitized server-side before save. Enumerate all locations where UGC is rendered and confirm that at least 100% use safe rendering (text node or sanitized HTML). Content is rendered as text, not HTML. No unsafe rendering patterns in templates. Quote the actual sanitization library or function used.
Fail criteria: User content is stored as-is and rendered with unsafe methods without sanitization. Using only client-side sanitization does not count as pass.
Skip (N/A) when: Never — XSS prevention is essential for any UGC platform.
Detail on fail: "Comments are rendered without sanitization. User-submitted HTML could execute arbitrary scripts."
Remediation: Always sanitize UGC content before storage. Use a library like dompurify:
import DOMPurify from 'dompurify';
async function submitComment(html, userId) {
// Sanitize on the backend
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href']
});
await db.comments.create({
content: sanitized,
userId,
createdAt: new Date()
});
}
On the frontend, treat content as plain text or use a safe rendering library:
// Safe: Content rendered as text
<div className="comment">{comment.content}</div>
// Unsafe: Avoid this
// <div dangerouslySetInnerHTML={{ __html: comment.content }} />
Always sanitize on the server-side before saving to prevent XSS attacks.