Code review example: Stored XSS: one malicious bio runs script in every visitor's browser in React

Retired from the DiffDojo practice pool in October 2026; the review below is the canonical answer. An AI wrote this pull request for the task underneath. Read the diff and decide what you would say before you scroll to the review.

React · Security · intermediate

The task

Render user bios with markdown

  1. Show each user's bio on their public profile.
  2. Bios are authored in markdown and stored as raw text; render them as formatted HTML.
  3. Use the existing marked() function to turn the markdown into an HTML string.

Renders user bios as formatted HTML. Runs the stored markdown through marked() and drops the result into the profile card so headings, links and bold text show up properly. Looks great on the profiles I tested.

Bios are free text that any signed-up user edits themselves; profile pages are public and shown to every visitor.

The diff

src/components/UserBio.jsx+10
1+ import { marked } from 'marked';
2+
3+ export function UserBio({ user }) {
4+ const html = marked(user.bio);
5+ return (
6+ <div className="bio">
7+ <div dangerouslySetInnerHTML={{ __html: html }} />
8+ </div>
9+ );
10+ }

The review

Issue 1: Stored XSS: one malicious bio runs script in every visitor's browser

src/components/UserBio.jsx, lines 4, 5, 6, 7, 8, 9, 10

  1. marked() does NOT sanitize. It converts markdown to an HTML string, and markdown allows raw HTML, so a bio like <img src=x onerror=...> or an inline event handler passes straight through.
  2. dangerouslySetInnerHTML injects it verbatim, attacker-controlled markup lands in every visitor's DOM, and the script runs with the victim's session. The name is the warning.
  3. It's persistent, not reflected. Bios are saved and replayed to everyone, so one malicious bio hits every profile viewer.

The fix: sanitize the generated HTML before rendering, e.g. DOMPurify.sanitize(marked(user.bio)), or configure a sanitizing markdown pipeline.

The fix

import { marked } from 'marked';
import DOMPurify from 'dompurify';

export function UserBio({ user }) {
  const html = DOMPurify.sanitize(marked(user.bio));
  return (
    <div className="bio">
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </div>
  );
}

Why it matters

dangerouslySetInnerHTML fed by user-controlled input → ask: "what sanitizes this?" React escapes text by default, so XSS usually only returns through an explicit escape hatch, exactly where 'render this markdown' nudges you.

Real-world stakes: an attacker edits their own bio to include a script payload, and it fires in the browser of every person who views their profile, including admins: cookie/session theft, silent account actions, or a self-propagating worm across profiles. It ships looking correct because ordinary bios render fine in every manual test.

Read more: dangerouslySetInnerHTML XSS sanitize, marked markdown sanitize DOMPurify, stored persistent XSS react, sanitize html before rendering

The verdict

Request changes. Stored XSS: one malicious bio runs script in every visitor's browser

Try a live one

React PRs you have not seen the answer to.

Leave inline comments and a verdict, then see what you caught. Free, no signup.

Review a React PR →

How to review React code · All code review examples