react-md-editor, published as @uiw/react-md-editor, is a popular React markdown editor with live preview, and the single most important thing to know about it is that its preview will render raw HTML by default, which means you must add sanitization or you have a cross-site scripting (XSS) hole. The library is well made and widely used, and the danger here is not a bug in it. It is the default behavior of markdown itself: the CommonMark spec permits inline HTML, so a markdown renderer that faithfully implements the spec will happily turn a <script> tag in the input into a live script tag in the output unless you tell it not to.
This guide covers what the editor does, exactly where the XSS risk comes from, and how to close it properly.
What @uiw/react-md-editor provides
The @uiw/react-md-editor package gives you a drop-in markdown editing component for React, with a split or toggled editor-and-preview view, a toolbar, syntax highlighting in the editor pane, and a preview pane that renders the markdown to HTML using the unified/remark/rehype pipeline underneath. It is TypeScript-native and themeable, which is why it shows up in a lot of documentation sites, note-taking apps, and comment systems.
A minimal usage looks like this:
import MDEditor from "@uiw/react-md-editor";
import { useState } from "react";
function Editor() {
const [value, setValue] = useState("**Hello world!**");
return <MDEditor value={value} onChange={setValue} />;
}
That works, and for content only you write (your own docs, your own notes) it is fine. The moment the markdown can come from someone else, that same code is a liability.
Where the XSS risk comes from
Markdown was designed to allow embedded HTML. That is a feature: it lets authors drop in a bit of raw markup when markdown's syntax is not enough. But it means the string a user types is not limited to headings and bold text. A malicious user can type an HTML <img> tag with an onerror handler, an inline event attribute, or a script element, and a faithful markdown renderer will pass that HTML through to the DOM.
If your app renders one user's markdown to another user's browser (comments, shared documents, profile bios, wiki pages), that is stored XSS: the attacker's payload is saved in your database and executes in the victim's session when they view the content. It can steal session tokens, make requests as the victim, or deface the page. This is one of the oldest and most damaging web vulnerability classes, and markdown editors are a classic vector precisely because developers assume "it's just markdown" and forget the HTML passthrough.
The library maintainers document this directly: markdown needs to be sanitized if you do not completely trust your authors, otherwise your app is vulnerable to XSS. The responsibility is placed on you, the integrator, because the library cannot know whether your content is trusted.
The fix: rehype-sanitize
Because the preview runs on the rehype pipeline, you close the hole by adding the rehype-sanitize plugin to the preview options. It walks the generated HTML tree and strips anything not on an allowlist of safe elements and attributes, so script tags, event handlers, and dangerous URLs are removed before they ever reach the DOM.
import MDEditor from "@uiw/react-md-editor";
import rehypeSanitize from "rehype-sanitize";
import { useState } from "react";
function SafeEditor() {
const [value, setValue] = useState("**Hello world!**");
return (
<MDEditor
value={value}
onChange={setValue}
previewOptions={{
rehypePlugins: [[rehypeSanitize]],
}}
/>
);
}
With that in place, if a user submits markdown containing a script or an onerror attribute, the sanitizer removes it before the preview renders, and the payload never executes. This is the official recommended pattern, and it should be treated as the default configuration for any deployment where the content is not fully trusted, not an optional hardening step.
If your app legitimately needs to allow some HTML (say, a specific set of tags for rich formatting), rehype-sanitize takes a schema so you can extend the allowlist deliberately rather than turning sanitization off. Widen the allowlist to exactly what you need and no more; the failure mode is quietly re-allowing a dangerous attribute.
Defense in depth around the editor
Sanitization at render time is the primary control, but a few habits make the whole feature more resilient.
Sanitize on the way in as well as on the way out where practical, so stored content is clean and a future rendering path that forgets the sanitizer is not automatically exploitable. Do not rely on client-side sanitization alone if the same content is ever rendered somewhere other than through this editor's preview; the server that serves the content should be sure it is safe regardless of which front end displays it.
Set a Content Security Policy that disallows inline scripts. A good CSP is a backstop: even if a sanitization gap slips through, a policy that blocks inline script execution blunts the impact. It is not a substitute for sanitizing, but it turns a critical XSS into a much smaller problem.
And keep the editor and its pipeline dependencies current. The unified/rehype ecosystem gets security-relevant fixes, and an out-of-date rehype-sanitize or a transitive markdown dependency can carry its own issues. This is the kind of transitive exposure an SCA tool such as Safeguard surfaces automatically, so a vulnerable dependency three levels below @uiw/react-md-editor does not sit unnoticed in your lockfile. Our Academy covers auditing front-end dependency chains in more detail.
FAQ
Is @uiw/react-md-editor vulnerable to XSS out of the box?
By default the preview renders raw HTML embedded in markdown, so if you display untrusted user content you have a cross-site scripting risk. This is markdown's HTML passthrough behavior, not a defect in the library. You close it by adding the rehype-sanitize plugin to the editor's preview options.
How do I make react-md-editor safe for user-generated content?
Add rehype-sanitize to previewOptions.rehypePlugins. It strips script tags, inline event handlers, and dangerous URLs from the rendered HTML before it reaches the DOM. Treat this as the default configuration for any content you do not fully trust, and back it up with a Content Security Policy that blocks inline scripts.
Can I still allow some HTML in the markdown?
Yes. rehype-sanitize accepts a schema so you can extend the allowlist to exactly the tags and attributes you need, rather than disabling sanitization entirely. Widen it deliberately and minimally; the risk is accidentally re-permitting a dangerous attribute like an inline event handler.
Do I need server-side sanitization too if I already sanitize in the preview?
If the content is ever rendered anywhere other than through this editor's preview, yes. Client-side sanitization only protects that one rendering path. Sanitizing on input or on the server ensures the stored content is safe regardless of which front end later displays it.