Cross-site scripting vulnerability in TinyMCE alerts
Description
tinymce is an open source rich text editor. A cross-site scripting (XSS) vulnerability was discovered in the alert and confirm dialogs when these dialogs were provided with malicious HTML content. This can occur in plugins that use the alert or confirm dialogs, such as in the image plugin, which presents these dialogs when certain errors occur. The vulnerability allowed arbitrary JavaScript execution when an alert presented in the TinyMCE UI for the current user. This vulnerability has been patched in TinyMCE 5.10.7 and TinyMCE 6.3.1 by ensuring HTML sanitization was still performed after unwrapping invalid elements. Users are advised to upgrade to either 5.10.7 or 6.3.1. Users unable to upgrade may ensure the the images_upload_handler returns a valid value as per the images_upload_handler documentation.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tinymcenpm | >= 6.0.0, < 6.3.1 | 6.3.1 |
tinymce/tinymcePackagist | >= 6.0.0, < 6.3.1 | 6.3.1 |
TinyMCENuGet | >= 6.0.0, < 6.3.1 | 6.3.1 |
tinymcenpm | < 5.10.7 | 5.10.7 |
tinymce/tinymcePackagist | < 5.10.7 | 5.10.7 |
TinyMCENuGet | < 5.10.7 | 5.10.7 |
Affected products
1Patches
28bb2d2646d4eMerge pull request from GHSA-gg8r-xjwq-4w92
7 files changed · +99 −1
modules/tinymce/CHANGELOG.md+3 −0 modified@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed +- HTML in messages for the `WindowManager.alert` and `WindowManager.confirm` APIs were not properly sanitized. #TINY-3548 + ## 5.10.6 - 2022-10-19 ### Fixed
modules/tinymce/package.json+1 −0 modified@@ -23,6 +23,7 @@ "silver-test-manual": "grunt bedrock-manual:silver" }, "dependencies": { + "dompurify": "2.3.8", "tslib": "^2.0.0" } }
modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts+10 −0 added@@ -0,0 +1,10 @@ +/** + * Copyright (c) Tiny Technologies, Inc. All rights reserved. + * Licensed under the LGPL or a commercial license. + * For LGPL see License.txt in the project root for license information. + * For commercial licenses see https://www.tiny.cloud/ + */ + +import createDompurify from 'dompurify'; + +export const sanitizeHtmlString = (html: string): string => createDompurify().sanitize(html);
modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts+2 −1 modified@@ -15,6 +15,7 @@ import { Class, SugarBody } from '@ephox/sugar'; import Env from 'tinymce/core/api/Env'; import { UiFactoryBackstageProviders } from '../../backstage/Backstage'; +import * as HtmlSanitizer from '../core/HtmlSanitizer'; import * as NavigableObject from '../general/NavigableObject'; const isTouch = Env.deviceType.isTouch(); @@ -84,7 +85,7 @@ const pBodyMessage = (message: string, providersBackstage: UiFactoryBackstagePro }, components: [ { - dom: DomFactory.fromHtml(`<p>${providersBackstage.translate(message)}</p>`) + dom: DomFactory.fromHtml(`<p>${HtmlSanitizer.sanitizeHtmlString(providersBackstage.translate(message))}</p>`) } ] }
modules/tinymce/src/themes/silver/test/ts/phantom/window/WindowManagerAlertTest.ts+39 −0 modified@@ -184,4 +184,43 @@ describe('phantom.tinymce.themes.silver.window.WindowManagerAlertTest', () => { Mouse.clickOn(SugarBody.body(), '.tox-button:contains("OK")'); UiFinder.notExists(SugarBody.body(), '[role="dialog"]'); }); + + it('TINY-3548: sanitize message', async () => { + createAlert('<a href="javascript:alert(1)">invalid link</a><script>alert(1)</script><a href="http://tiny.cloud">valid link</a>', Fun.noop); + const dialogBody = SelectorFind.descendant(SugarDocument.getDocument(), '.tox-dialog__body').getOrDie('Cannot find dialog body element'); + Assertions.assertStructure('A basic alert dialog should have these components', + ApproxStructure.build((s, str, arr) => s.element('div', { + classes: [ arr.has('tox-dialog__body') ], + children: [ + s.element('div', { + classes: [ arr.has('tox-dialog__body-content') ], + children: [ + s.element('p', { + children: [ + s.element('a', { + attrs: { + href: str.none('Should have been trimmed away') + }, + children: [ + s.text(str.is('invalid link')) + ] + }), + s.element('a', { + attrs: { + href: str.is('http://tiny.cloud') + }, + children: [ + s.text(str.is('valid link')) + ] + }) + ] + }) + ] + }) + ] + })), + dialogBody + ); + await pTeardown(); + }); });
modules/tinymce/src/themes/silver/test/ts/phantom/window/WindowManagerConfirmTest.ts+39 −0 modified@@ -195,4 +195,43 @@ describe('phantom.tinymce.themes.silver.window.WindowManagerConfirmTest', () => Mouse.clickOn(SugarBody.body(), '.tox-button:contains("Yes")'); UiFinder.notExists(SugarBody.body(), '[role="dialog"]'); }); + + it('TINY-3548: sanitize message', async () => { + createConfirm('<a href="javascript:alert(1)">invalid link</a><script>alert(1)</script><a href="http://tiny.cloud">valid link</a>', Fun.noop); + const dialogBody = SelectorFind.descendant(SugarDocument.getDocument(), '.tox-dialog__body').getOrDie('Cannot find dialog body element'); + Assertions.assertStructure('A basic alert dialog should have these components', + ApproxStructure.build((s, str, arr) => s.element('div', { + classes: [ arr.has('tox-dialog__body') ], + children: [ + s.element('div', { + classes: [ arr.has('tox-dialog__body-content') ], + children: [ + s.element('p', { + children: [ + s.element('a', { + attrs: { + href: str.none('Should have been trimmed away') + }, + children: [ + s.text(str.is('invalid link')) + ] + }), + s.element('a', { + attrs: { + href: str.is('http://tiny.cloud') + }, + children: [ + s.text(str.is('valid link')) + ] + }) + ] + }) + ] + }) + ] + })), + dialogBody + ); + await pTeardown(); + }); });
yarn.lock+5 −0 modified@@ -4243,6 +4243,11 @@ domhandler@^2.3.0: dependencies: domelementtype "1" +dompurify@2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.8.tgz#224fe9ae57d7ebd9a1ae1ac18c1c1ca3f532226f" + integrity sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw== + domutils@^1.5.1, domutils@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
6923d85eba6dMerge pull request from GHSA-gg8r-xjwq-4w92
5 files changed · +82 −1
modules/tinymce/CHANGELOG.md+3 −0 modified@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Fixed +- HTML in messages for the `WindowManager.alert` and `WindowManager.confirm` APIs were not properly sanitized. #TINY-3548 + ## 6.3.0 - 2022-11-23 ### Added
modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts+3 −0 added@@ -0,0 +1,3 @@ +import createDompurify from 'dompurify'; + +export const sanitizeHtmlString = (html: string): string => createDompurify().sanitize(html);
modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts+2 −1 modified@@ -8,6 +8,7 @@ import { Class, SugarBody } from '@ephox/sugar'; import Env from 'tinymce/core/api/Env'; import { UiFactoryBackstageProviders } from '../../backstage/Backstage'; +import * as HtmlSanitizer from '../core/HtmlSanitizer'; import * as NavigableObject from '../general/NavigableObject'; const isTouch = Env.deviceType.isTouch(); @@ -77,7 +78,7 @@ const pBodyMessage = (message: string, providersBackstage: UiFactoryBackstagePro }, components: [ { - dom: DomFactory.fromHtml(`<p>${providersBackstage.translate(message)}</p>`) + dom: DomFactory.fromHtml(`<p>${HtmlSanitizer.sanitizeHtmlString(providersBackstage.translate(message))}</p>`) } ] }
modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts+37 −0 modified@@ -184,4 +184,41 @@ describe('headless.tinymce.themes.silver.window.WindowManagerAlertTest', () => { Mouse.clickOn(SugarBody.body(), '.tox-button:contains("OK")'); UiFinder.notExists(SugarBody.body(), '[role="dialog"]'); }); + + it('TINY-3548: sanitize message', async () => { + createAlert('<a href="javascript:alert(1)">invalid link</a><script>alert(1)</script><a href="http://tiny.cloud">valid link</a>', Fun.noop); + const dialogBody = SelectorFind.descendant(SugarDocument.getDocument(), '.tox-dialog__body').getOrDie('Cannot find dialog body element'); + Assertions.assertStructure('A basic alert dialog should have these components', + ApproxStructure.build((s, str, arr) => s.element('div', { + classes: [ arr.has('tox-dialog__body') ], + children: [ + s.element('div', { + classes: [ arr.has('tox-dialog__body-content') ], + children: [ + s.element('p', { + children: [ + s.element('a', { + exactAttrs: { }, + children: [ + s.text(str.is('invalid link')) + ] + }), + s.element('a', { + exactAttrs: { + href: str.is('http://tiny.cloud') + }, + children: [ + s.text(str.is('valid link')) + ] + }) + ] + }) + ] + }) + ] + })), + dialogBody + ); + await pTeardown(); + }); });
modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts+37 −0 modified@@ -195,4 +195,41 @@ describe('headless.tinymce.themes.silver.window.WindowManagerConfirmTest', () => Mouse.clickOn(SugarBody.body(), '.tox-button:contains("Yes")'); UiFinder.notExists(SugarBody.body(), '[role="dialog"]'); }); + + it('TINY-3548: sanitize message', async () => { + createConfirm('<a href="javascript:alert(1)">invalid link</a><script>alert(1)</script><a href="http://tiny.cloud">valid link</a>', Fun.noop); + const dialogBody = SelectorFind.descendant(SugarDocument.getDocument(), '.tox-dialog__body').getOrDie('Cannot find dialog body element'); + Assertions.assertStructure('A basic alert dialog should have these components', + ApproxStructure.build((s, str, arr) => s.element('div', { + classes: [ arr.has('tox-dialog__body') ], + children: [ + s.element('div', { + classes: [ arr.has('tox-dialog__body-content') ], + children: [ + s.element('p', { + children: [ + s.element('a', { + exactAttrs: { }, + children: [ + s.text(str.is('invalid link')) + ] + }), + s.element('a', { + exactAttrs: { + href: str.is('http://tiny.cloud') + }, + children: [ + s.text(str.is('valid link')) + ] + }) + ] + }) + ] + }) + ] + })), + dialogBody + ); + await pTeardown(); + }); });
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-gg8r-xjwq-4w92ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23494ghsaADVISORY
- github.com/tinymce/tinymce/commit/6923d85eba6de3e08ebc9c5a387b5abdaa21150eghsax_refsource_MISCWEB
- github.com/tinymce/tinymce/commit/8bb2d2646d4e1a718fce61a775fa22e9d317b32dghsax_refsource_MISCWEB
- github.com/tinymce/tinymce/security/advisories/GHSA-gg8r-xjwq-4w92ghsax_refsource_CONFIRMWEB
- www.tiny.cloud/docs/release-notes/release-notes5107/ghsax_refsource_MISCWEB
- www.tiny.cloud/docs/tinymce/6/6.3-release-notes/ghsax_refsource_MISCWEB
- www.tiny.cloud/docs/tinymce/6/file-image-upload/ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.