13 min read
High
By

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.

AI-generated · gpt-image 2.0

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:protected comments with the protect option enabled). The class addressed here: an attacker with editor rights writes JavaScript payload into data-mce-href, data-mce-src or data-mce-style; on the next serialisation pass TinyMCE copies those values back into the visible href/src/style attributes 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/-style values.

Am I affected as a Moselwal customer?

Moselwal's own TYPO3 stack is unaffected — we ship TYPO3 with CKEditor 5 (rte_ckeditor since v8 LTS), not TinyMCE. Across the PHP stack the default editors are predominantly CKEditor: Sonata Admin uses CKEditor via SonataFormatterBundle plus FOSCKEditorBundle, Sylius uses CKEditor via FOSCKEditorBundle, 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/tinymce or the npm package tinymce, 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 injected javascript: 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

We are not writing this post out of being affected ourselves. Moselwal ships 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. Our 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 us: Moselwal builds 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 our technical and strategic assessment. It does not replace legal counsel or a data-protection impact assessment.

Sources

About the author

[Translate to English:] Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Founder · Moselwal Digitalagentur · OnlyOle

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012, now Moselwal. Over 100 projects, with a focus on security, performance, automation and quality.