CVE-2026-47759: TinyMCE Stored XSS via data-mce-* Attributes — Why the Rich-Text Editor Has Just Become Its Own Supply-Chain Layer in Every PHP Admin Backend
29 May 2026. TinyMCE shipped 5.11.1, 7.9.3 and 8.5.1 yesterday, closing several stored-XSS findings — the most prominent is CVE-2026-47759 (CVSS 8.7, high) via unsanitised data-mce-href, data-mce-src and data-mce-style attributes. The bug leans on a documented TinyMCE property: the editor writes these internal bookkeeping attributes back into the visible href/src/style values during serialisation, overriding a previously clean HtmlPurifier or DOMPurify check. TYPO3 default installs are not affected (CKEditor 5 instead of TinyMCE), and Sonata Admin, Sylius and Drupal each ship CKEditor as their default; the exposed audience is primarily WordPress sites with the Classic Editor plugin enabled and individual PHP backends that embedded TinyMCE directly. For the Mittelstand supply chain the lesson is concise: as of today the rich-text editor is its own layer in the SBOM alongside Composer and npm dependencies.

TL;DR — the 90-second summary
- What was disclosed?
CVE-2026-47759 (GHSA disclosure 28 May 2026), a stored cross-site scripting flaw in the TinyMCE rich-text editor. The same release cycle also covers CVE-2026-47761 (high, additional stored-XSS class) and CVE-2026-47762 (medium,
mce:protectedcomments with theprotectoption enabled). The class addressed here: an attacker with editor rights writes JavaScript payload intodata-mce-href,data-mce-srcordata-mce-style; on the next serialisation pass TinyMCE copies those values back into the visiblehref/src/styleattributes and overrides markup that an external sanitiser had already approved as “clean”.- How serious?
High (CVSS 8.7). The authentication bar is “editor rights” — an author or editor login that feeds the editor in regular use. In any multi-editor CMS where posts travel between roles (author → editor → publishing), this class path is real. From the entry account onward, the stealer payload is exfiltrated to the cookies and tokens of every higher-privileged role and, where applicable, every frontend visitor, as soon as the markup reaches a render run.
- Which TinyMCE versions are affected?
All versions before 5.11.1, 7.9.3 and 8.5.1 — i.e. the running stable branches 5.x (LTS maintenance), 7.x (previous major) and 8.x (current major). Fix: upgrade to 5.11.1, 7.9.3 or 8.5.1, plus an audit pass across all stored posts looking for already injected
data-mce-href/-src/-stylevalues.- Am I affected?
My own TYPO3 stack is unaffected — I ship TYPO3 with CKEditor 5 (
rte_ckeditorsince v8 LTS), not TinyMCE. Across the PHP stack the default editors are predominantly CKEditor: Sonata Admin uses CKEditor viaSonataFormatterBundleplusFOSCKEditorBundle, Sylius uses CKEditor viaFOSCKEditorBundle, Drupal 10 ships CKEditor 5 as the default (TinyMCE only exists as a contrib module for Drupal 8/9). You are directly affected where one of the following sits in your platform: WordPress sites with the Classic Editor plugin enabled (by far the largest exposed class worldwide), individually developed PHP admin backends, third-party SaaS admins (newsletter tools, helpdesks, wiki engines), or a Sonata/Sylius/Drupal setup where TinyMCE has been configured as a deliberate alternative to CKEditor.- Immediate mitigation?
Three steps. First, check the TinyMCE version in each backend and upgrade to 5.11.1, 7.9.3 or 8.5.1 (or higher) — update the Composer package
tinymce/tinymceor the npm packagetinymce, depending on the integration path. Second, audit sweep across stored content: search the database for the three attribute strings (data-mce-href,data-mce-src,data-mce-style) in every RTE field, then review the hits for injectedjavascript:URLs,expression()constructs or external script hosts. Third, set a strict Content-Security-Policy on the editor and publishing domain (script-src 'self') if it is not already in place.- Criticality?
See the hero badge
high— act within the 48-hour window because the disclosure describes the bug publicly and the sanitiser bypass is reproducible. Active exploitation in the wild is not documented as of this brief; no CISA KEV entry yet.
What happened
Tiny Technologies (formerly Ephox, the maker of TinyMCE) closed several stored-XSS findings in the TinyMCE rich-text editor on 28 May 2026 with three parallel releases: 5.11.1 (LTS branch), 7.9.3 (previous major) and 8.5.1 (current major). At least three CVE numbers appear in this release cycle: CVE-2026-47759 (CVSS 8.7, high) via the data-mce-href/-src/-style attributes, CVE-2026-47761 (high) as an additional stored-XSS class, CVE-2026-47762 (medium) via mce:protected comments when the protect option is enabled. This post focuses on CVE-2026-47759 as the methodologically most interesting and the most broadly exposed class — the three affected attributes are data-mce-href, data-mce-src and data-mce-style.
The mechanism uses the interplay between TinyMCE's editor bookkeeping and any downstream HTML sanitiser. During parsing TinyMCE writes the href, src and style values additionally as internal data-mce-href, data-mce-src and data-mce-style attributes (official documentation: “TinyMCE converts src and href into data-mce-src, data-mce-href and data-mce-style as internal attributes”). These temporary attributes are designed to be removed at the final getContent(), but they survive certain round-trip paths (saving, drag-and-drop, copy-paste, plugin re-activation). On those paths TinyMCE then serialises the data-mce-* values authoritatively back into the visible attributes. This is not a design choice for the final value; it is helper behaviour for internal UI round-trips — and exactly that invisible layer has never been sanitiser-checked.
The security consequence had been quietly hiding. A downstream HTML sanitiser (HtmlPurifier, DOMPurify, a platform-specific allowlist) checks the href end value, the src end value or the style end value against its allowlist. A javascript: URL is rejected, an expression() style rule is rejected, an external script host is rejected. If, on the other hand, a data-mce-href="javascript:..." survives in the markup — because the sanitiser does not know the attribute or treats it as a harmless data attribute — and the server stores the markup that way, TinyMCE then overrides the previously checked href value with the data-mce-href payload on the next serialisation pass. The clean check is overruled from the back end.
The patches close the gap with extended sanitising logic in the serialisation pipeline: TinyMCE now checks the data-mce-* attributes themselves against the same allowlist as the visible href/src/style values. From a platform operator's perspective the second line of defence remains mandatory — the downstream sanitiser has to know the data-mce-* attributes, and the markup in the database has to be checked once for already injected payload before the platform declares the update complete.
Technical analysis
Structurally CVE-2026-47759 is not a classical bug class but a sanitiser-bypass weakness caused by a design difference between the editor bookkeeping and the downstream sanitising layer. The generalisable pattern: an editor persists more state than the visible HTML surface suggests, and a sanitiser has to either know or explicitly drop that invisible state set. The pattern shows up repeatedly in the rich-text editor world with different mechanisms — CVE-2018-9861 covered the CKEditor Image2 plugin via crafted IMG elements (different mechanism, same pattern class: “editor-internal markup survives the sanitiser”), CVE-2023-26149 covered the quill-mention add-on via unsanitised render-list data. Anyone who persists rich-text editor markup in their platform should adopt the reflex from today onward that editor-specific helper attributes form their own audit class.
Methodologically the important shift is the trust-boundary placement. If you run HtmlPurifier or your own allowlist in your PHP backend and rely on the href/src/style end value, you have placed the trust boundary at the end value. CVE-2026-47759 shows that for TinyMCE markup this line was drawn too tightly — the data-mce-* attributes belong in the same check track. Concretely: HtmlPurifier configuration must set HTML.AllowedAttributes so that data-mce-* attributes are either dropped entirely (standard hardening) or pushed through the same URI/CSS allowlist. DOMPurify users set ADD_URI_SAFE_ATTR and ALLOWED_URI_REGEXP in the same spirit. Platforms with their own allowlist take the data-mce-* class into the allowlist code now.
The link to the supply-chain track of the past two weeks is the methodologically interesting situation. After the TanStack npm wave (11 May), Mini-Shai-Hulud @antv (19 May), the Nx Console VS Code marketplace wave (18 May) and the vpmdhaj OpenSearch/ElasticSearch typosquat (28 May), CVE-2026-47759 represents a different supply-chain class: not a compromised distribution layer, but a trust layer built into the component itself that lifts an assumption away from downstream security layers — layers that treat the assumption as safe. Anyone whose SBOM only tracks Composer and npm dependencies has implicitly checked off the rich-text editor as “a thing from npm” — yet in the browser it remains its own security domain, and its trust boundary has to be aligned with the platform sanitiser.
What this means for the Mittelstand
I am not writing this post out of being affected myself. I ship TYPO3 with CKEditor 5 — the rte_ckeditor extension that has been the default editor since TYPO3 v8 LTS and was upgraded to CKEditor 5 in v12. My TYPO3 default installs do not carry TinyMCE in the render path. Across the wider PHP stack of the German Mittelstand the picture is clearly delimited — and that is the point where many first reflexes land off the mark.
First class, broadly distributed: WordPress with the Classic Editor plugin enabled. WordPress has shipped the Block Editor (Gutenberg) as the default since 5.0, and Gutenberg does not carry TinyMCE as its central rich-text engine. The Classic Editor plugin reactivates the TinyMCE-based Edit Post surface and is, with double-digit million active installations, one of the most widely used reactivation paths for the classical editor model. For a WordPress tenant the reflex question becomes: is Classic Editor running, and how current is the editor stack?
Second class, narrower: individually developed PHP admin backends. Sonata Admin uses CKEditor via SonataFormatterBundle plus FOSCKEditorBundle as the default editor; Sylius uses CKEditor via FOSCKEditorBundle in the standard admin; Drupal 10 ships CKEditor 5 as the default (TinyMCE exists as a contrib module for Drupal 8/9; for Drupal 10 it is, as of 05/2026, not stably ported). In these three stacks TinyMCE is only in the picture if the project has made a deliberate decision against the default editor and embedded TinyMCE manually — because of a Tiny Cloud licence or a legacy code path, say. Add to that custom admin backends that embedded TinyMCE directly and third-party SaaS admins (helpdesk tools, newsletter platforms, wiki engines) that ship TinyMCE as an editor component.
Compliance-wise the finding plays out on the standard axes. GDPR Art. 32 applies to any platform whose editor path processes personal data — customer support tickets, CRM notes, newsletter posts addressed to recipient segments, wiki entries about staff. A stored-XSS path that exfiltrates editor or admin cookies is, in the language of Annex 1 to Art. 32, a technical deficit in the confidentiality of processing. NIS-2 Art. 21 requires supply-chain discipline in the wider sense; rich-text editors qualify as components of the deployed software, and an SBOM that omits TinyMCE does not carry the supply-chain inventory in full. For DORA-bound and MaRisk-bound organisations the editor sits squarely in the assessment of third-party components used.
What this means for technical development
Architecturally CVE-2026-47759 forces an honest SBOM inventory. Composer and npm packages have been machine-readable in nearly all Mittelstand pipelines for two years — cyclonedx-php-composer, cyclonedx-bom for npm, GitHub Dependabot, Mend-style tools. Rich-text editors usually sit a layer below: they are installed as Composer packages (tinymce/tinymce) but appear in the application code as a component inside an editor wrapper library (for example sonata-project/formatter-bundle). If you maintain the SBOM at the Composer lockfile level only, you either do not see TinyMCE at all or you see it as a secondary dependency without an own risk score. The lesson for the pipeline: SBOM tools have to surface the transitive JavaScript bundles inside the application explicitly.
Methodologically the second lesson sits in the trust-boundary discussion. HtmlPurifier and DOMPurify are the standard sanitisers in the PHP and JavaScript worlds; their default configuration reliably refuses script tags, javascript: URLs and CSS expression() constructs. Until now the unspoken consensus was: if the sanitiser configuration is current and the allowlist is clean, editor markup is under control. CVE-2026-47759 shifts that assumption — the data-mce-* class shows that editor bookkeeping attributes belong in the sanitiser check, even though they appear neither in the HTML standard nor in any allowlist default. The generalisable lesson: sanitiser configuration for rich-text editor markup should be editor-specific and aware of the bookkeeping classes (CKEditor: data-cke-*, TinyMCE: data-mce-*, Quill: data-quill-*, ProseMirror: data-pm-*). A sanitiser config that is missing these classes is not “stricter than necessary”; it has a blind spot.
Third, Content-Security-Policy is the second line of defence. Anyone setting Content-Security-Policy: script-src 'self' without unsafe-inline in the backend catches a surviving XSS path at the browser layer, because a javascript: URL out of a href override cannot trigger execution from a click. This discipline has been default in TYPO3 backends since v12; in Sonata Admin and Drupal backends it needs to be configured. Anyone using CVE-2026-47759 as the prompt to raise the CSP headers in their own admin paths to a strict default has already half-patched the next sanitiser-bypass class.
Concrete recommendations
In this order. First, inventory today where TinyMCE runs in your platform landscape — a single find . -name "tinymce.min.js" across the project asset directory surfaces the statically embedded paths, composer why tinymce/tinymce and npm ls tinymce cover the package paths. Second, for every hit: check the version, upgrade to 5.11.1, 7.9.3 or 8.5.1 (depending on the major), clear caches, test the editor function. Third, audit pass across stored RTE content: search the database for the three attribute strings (data-mce-href, data-mce-src, data-mce-style) across every table with rich-text columns — in WordPress typically wp_posts.post_content and wp_postmeta; in Symfony/Sylius the *_translation tables with description/content columns; in Drupal node__body/paragraph__field_text. Review the hits for javascript: URLs, external script hosts and CSS expression() constructs. Fourth, check the platform's sanitiser configuration: set HtmlPurifier HTML.AllowedAttributes or DOMPurify ALLOWED_ATTR so that data-mce-* is either dropped entirely or runs through the same URI/CSS allowlist as the visible attributes. Fifth, Content-Security-Policy for the editor backends: script-src 'self' without unsafe-inline, style-src 'self' with concrete hashes or nonces. Sixth, document TinyMCE and every other rich-text editor in your SBOM process; if you do not have an SBOM yet, this is the prompt to start one. If these steps do not run on their own, talk to me: I build platforms in which rich-text editors are tracked as their own supply-chain class and run with hardened sanitiser and CSP configuration.
This post reflects my technical and strategic assessment. It does not replace legal counsel or a data-protection impact assessment.
Sources
- Tiny Technologies — TinyMCE Changelog (release notes 8.5.1 / 7.9.3 / 5.11.1, 28 May 2026)
- tinymce/tinymce — GitHub Security Advisories (CVE-2026-47759 / -47761 / -47762, 28 May 2026)
- The Hacker Wire — TinyMCE Stored XSS: data-mce-* Attribute Bypass (28 May 2026)
- DailyCVE — TinyMCE Stored XSS CVE-2026-47761 (28 May 2026)
- Tiny Technologies — tinymce.dom.Serializer API (source for the internal data-mce-* round-trip behaviour)
About the author
![[Translate to English:] Foto von Kai Ole Hartwig.](/fileadmin/_processed_/e/9/csm_ole-neu_73323ad80d.jpeg)
Kai Ole Hartwig
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.
