Acknowledgements & Refs
- Vulnerability security advisory: https://github.com/tinymce/tinymce/security/advisories/GHSA-q742-qvgc-gc2f
- TinyMCE patch release notes: https://www.tiny.cloud/docs/tinymce/latest/8.5.1-release-notes/#fixed-stored-xss-vulnerability-through-data-mce-prefixed-src-href-style-attributes
- Affected packages: https://security.snyk.io/vuln?search=cve-2026-47759
Background: What Is TinyMCE?
TinyMCE is a rich text editor that runs inside a web browser. You have probably used it without knowing as it is embedded behind the text boxes on platforms multiple major platforms as it just does its thing behind the scenes.
Because TinyMCE accepts HTML input from users, it needs to sanitize that input. This means it must strip out anything dangerous before the content is saved or shown to other people. If a user could sneak malicious code into a note or post or whatever the application is used for, that code could run in the browsers of everyone who later reads it.
The biggest issue I have seen so far with this integration is the implicit trust granted to TinyMCE by the developers that embed it into their applications. Server side input validation becomes an afterthought, assuming TinyMCE has enough filtering on the frontend to clean up any malicious input from users. This is where the XSS disaster begins.
This post serves as an initial synopsis to the vulnerability I discovered. I will dive into the code in the next post, then discovery in the third & last post of the series as that is a longer discussion.
The Vulnerability
How Paste Works in TinyMCE
When you copy something from a web page and paste it into TinyMCE’s rich text editor (we will just call it TinyMCE in this post), the editor does not just dump the raw clipboard content into the document. It runs the pasted HTML through a sanitization pipeline:
- The browser hands TinyMCE the clipboard content as raw HTML. Checkout MIME types for clipboard content here.
- TinyMCE passes it through DOMPurify — a well-known HTML sanitisation library to strip dangerous elements such as
<script>tags and event handlers (onerror,onload, etc.). - TinyMCE’s own parser processes the cleaned HTML into its internal format.
- When the user saves the note, TinyMCE serializes its internal format back to HTML, and that HTML is what gets stored.
This pipeline works correctly for most attacks. Script tags are removed. Event handlers are stripped. The problem was in a part of TinyMCE’s internal working that DOMPurify entirely skipped over.
TinyMCE’s Internal Attributes
TinyMCE uses a set of special attributes on HTML elements for its own internal bookkeeping. The three relevant ones are:
data-mce-src: used to track the originalsrcattribute of images and iframes before TinyMCE converts itdata-mce-href: used to track the originalhrefattribute of linksdata-mce-style: used to track the originalstyleattribute
These attributes are TinyMCE’s private notes to itself, so to speak. During the parse phase, TinyMCE looks at the real src or href value, converts it if needed (for example, making relative URLs absolute), and stores the original in data-mce-src / data-mce-href. When the content is later serialized back to HTML for saving, TinyMCE reads from data-mce-src / data-mce-href and writes that value back into the real src / href attribute.
The important word there is reads. During serialization, TinyMCE treats whatever is in data-mce-href as the authoritative source of truth for the link’s destination. It does not run URL validation at this stage, it assumes the value is safe because TinyMCE itself put it there.
As you might have guessed, this is where the magic happens.
The Flaw
DOMPurify is configured in TinyMCE to allow all data-* attributes through unchecked. This is standard practice as data-* attributes are a generic HTML mechanism for storing custom data, and blocking them wholesale would break many legitimate uses.
The problem is that data-mce-href, data-mce-src, and data-mce-style are also data-* attributes. An attacker who crafts clipboard content containing these attributes can slip them through DOMPurify entirely.
Another example of wildcard allow lists causing problems, lol.
Once they are through DOMPurify, TinyMCE’s parser encounters them. The parser has a check that said something along the lines of: “if this node already has a data-mce-href, do not overwrite it.”
I assumed the intention was to avoid redundant processing, but the effect is that the attacker’s malicious value survives untouched.
At serialization time, TinyMCE reads the attacker-controlled data-mce-href value which contains a payload such as javascript:alert(document.cookie) and writes it into the href attribute of the final output HTML.
The stored note now contains a link that executes JavaScript when clicked.
The Attack in One Sentence
An attacker pastes HTML like this into TinyMCE:
<a data-mce-href="javascript:alert(document.cookie)" href="<https://safe.example.com>">Click here</a>The visible link destination is safe.example.com.
The hidden data-mce-href carries the payload. TinyMCE sanitises the href and ignores it, then uses data-mce-href during save, so the stored content becomes:
<a href="javascript:alert(document.cookie)">Click here</a>Anyone who reads the note and clicks the link executes the attacker’s JavaScript.
In a lot of the downstream applications that I tested, iframes survived, allowing execution on load without user interaction. Yikes.
Why This Matters
This is exploitable in two scenarios:
- Your basic Stored XSS, that will allow client side code execution inside a victim’s browser. This can be exploited by you, as the attacker, by pasting the payload directly in a TinyMCE rich text editor in an application that eventually saves the content.
- Pastejacking: This attack requires the victim to copy and paste crafted content that they do not know is malicious into TinyMCE. An attacker hosts a page that looks like an ordinary document (meeting notes, a shared article) but embeds malicious
data-mce-*attributes in its HTML. The visible text looks completely normal. When the victim copies and pastes the content into a TinyMCE-backed application, the payload is saved and executed in the user’s browser without their knowledge. Beautiful if you ask me.
I confirmed the attack against TinyMCE versions 6.x, 7.9.2, and 8.4.0 across all three supported release lines at the time, prior to v8.5.1, v7.9.3, and v5.11.1.
The team was quick to respond and push a fix. Their release notes can be found here.