How do you safely show untrusted HTML in a React settings preview without opening XSS holes?

What’s up everyone? I’m wiring up a “settings preview” panel in React where users can paste a snippet of HTML (email template-ish) and see it rendered live, but I’m trying to keep the security boundaries sane.

const Preview = ({ html }: { html: string }) => (
  <div className="preview" dangerouslySetInnerHTML={{ __html: html }} />
);

If I need basic formatting (links, bold, lists) but want to prevent script/event-handler injection and also avoid breaking legit markup, what’s the practical approach you’d ship here (sanitize library, iframe sandbox, or something else) and why?

For an “email template-ish” preview, I’d ship a sanitizer first (DOMPurify) and treat the allowed tags/attrs like a tiny whitelist, because dangerouslySetInnerHTML is basically letting players paste mods straight into your save file. The part people forget is URLs: you want to strip javascript: and friends, and probably force a tags to rel="noopener noreferrer" (and maybe target="_blank" only if you really need it). DOMPurify has hooks for that, so you can keep links/bold/lists without letting onerror= or weird URL schemes slip through. Something like:

import DOMPurify from "dompurify";

const sanitize = (html: string) =>
  DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ["a", "b", "strong", "i", "em", "u", "p", "br", "ul", "ol", "li", "span"],
    ALLOWED_ATTR: ["href", "title"],
  });

const Preview = ({ html }: { html: string }) => {
  const clean = sanitize(html);
  return <div className="preview" dangerouslySetInnerHTML={{ __html: clean }} />;
};

And then (depending on your DOMPurify setup) you can add a hook to normalize links:

DOMPurify.addHook("afterSanitizeAttributes", (node) => {
  if (node.tagName === "A") {
    const href = node.getAttribute("href") || "";

    // DOMPurify already blocks a lot of bad schemes, but I still like being explicit.
    if (!/^https?:|^mailto:|^tel:/i.test(href)) node.removeAttribute("href");

    node.setAttribute("rel", "noopener noreferrer");
    // node.setAttribute("target", "_blank"); // only if you actually want new tabs
  }
});

I’d only go iframe sandbox if you truly need to support “real” email HTML (tables, inline styles, etc. ) or you don’t trust your whitelist to stay tight over time—because then you’re isolating the mess.