In short. The TinyMCE media plugin builds a preview element by assigning attacker-controlled data-mce-html to innerHTML on a live element, and it does this before the sanitizer runs. An <img onerror=...> placed in editor content therefore executes in the hosting page's origin during serialization, when editor.getContent() is called. The returned HTML string is clean, which is why the bug is easy to miss. Fixed in 8.9.3, rated High (GHSA-mf2p-h6hf-fcwm).
Background
TinyMCE is one of the most widely embedded rich-text editors. Its optional media plugin renders embeds (video, audio, iframes) inside the editor. To do that it keeps the real markup in a data-mce-html attribute on a placeholder element and rebuilds a preview from it. The rebuild is where this goes wrong.
The exposure is not plain display of stored editor output. It is content that travels back through an editor, which is the normal pattern for collaborative editing, review workflows and re-editing. Untrusted HTML enters an editor through setContent() or the initial value, and is later serialized through getContent(), which is fired automatically by editor.save() and form submit, by autosave, and by preview.
Preconditions
media plugin enabled (opt-in, but commonly enabled; no other non-default option needed)
untrusted HTML round-trips through an editor (setContent/initial value, then getContent)Root cause: sanitize runs too late
In the media plugin's buildMediaElement(), the attacker-controlled data-mce-html is written to innerHTML on a live element created with document.createElement(), and only afterwards does Sanitize.parseAndSanitize() run. Assigning to innerHTML on a live document parses and activates the markup immediately, so an onerror handler fires before the sanitizer ever sees it.
Proof of concept
On TinyMCE 8.5.1 or later, initialised with plugins: 'media', run this in the browser console.
var ed = tinymce.activeEditor || tinymce.editors[0];
ed.setContent('<span data-mce-object="video" data-mce-html="<img src=dave onerror=alert(document.domain)>">x</span>');
ed.getContent();The alert(document.domain) executes in the page origin, while getContent() returns a clean <p><video><img src="dave"></video></p>. I verified every reproduction path end to end against the 8.9.2 bundle.
Affected versions
This is a regression, not a long-standing issue. The vulnerable construction was introduced in the media refactor shipped in 7.9.3 and 8.5.1. Everything up to and including 7.9.2 and 8.5.0 is safe, because those releases pass the raw string to the sanitizer before building any DOM.
Impact and severity
An attacker who can get HTML into an editor that is later serialized runs script in the hosting page's origin, in the context of whoever is editing. In a review or collaboration flow that is another user, often a privileged one.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N -> 8.7 (High)
CWE-79The vector matches the one the maintainers assigned to the comparable sanitizer-bypass issue GHSA-mh5m-5hw4-5c69.
The fix
Sanitize data-mce-html before any innerHTML assignment, and build the preview element through an inert path (an AST, DOMParser, or <template>.content) rather than a live document.createElement, which is how the editor behaved before the refactor. The issue is fixed in 8.9.3 and tracked as GHSA-mf2p-h6hf-fcwm.
Disclosure
I reported this privately to the Tiny security team on 24 September 2026 under their coordinated-disclosure policy, and kept it confidential until the fix shipped. The team confirmed the issue the next day and asked how I would like to be credited in the advisory. They published GHSA-mf2p-h6hf-fcwm on 7 October 2026. My thanks to the team for the clear, professional handling throughout.
Timeline
2026-09-24: reported privately to the Tiny security team
2026-09-25: confirmed by the maintainers
2026-10-07: GHSA-mf2p-h6hf-fcwm published, fixed in 8.9.3
2026-10-13: this write-up publishedReferences
- Advisory: GHSA-mf2p-h6hf-fcwm
- Comparable prior issue: GHSA-mh5m-5hw4-5c69