CVE-2022-23494: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: 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.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-12-08T22:15Z
Repository: https://github.com/tinymce/tinymce
Language: TypeScript
Files changed: modules/tinymce/CHANGELOG.md, modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts, modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts, modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts, modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts
Commit date: 2022-12-06 11:02:46 +1000
Commit message: Merge pull request from GHSA-gg8r-xjwq-4w92

* TINY-3548: sanitize html in alert/confirm messages

* TINY-3548: updated changelog

* TINY-3548: updated changelog

* TINY-3548: updated changelog
Diff: diff --git a/modules/tinymce/CHANGELOG.md b/modules/tinymce/CHANGELOG.md
index 5118c398bd..b8030aeb9c 100644
--- a/modules/tinymce/CHANGELOG.md
+++ b/modules/tinymce/CHANGELOG.md
@@ -4,10 +4,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## 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
 - New `expand` function added to `tinymce.selection` which expands the selection around the nearest word. #TINY-9001
 - New `expand` function added to `tinymce.dom.RangeUtils` to return a new range expanded around the nearest word. #TINY-9001
diff --git a/modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts b/modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts
new file mode 100644
index 0000000000..e3d2f0b105
--- /dev/null
+++ b/modules/tinymce/src/themes/silver/main/ts/ui/core/HtmlSanitizer.ts
@@ -0,0 +1,3 @@
+import createDompurify from 'dompurify';
+
+export const sanitizeHtmlString = (html: string): string => createDompurify().sanitize(html);
diff --git a/modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts b/modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts
index 2343555af4..7ab01383f3 100644
--- a/modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts
+++ b/modules/tinymce/src/themes/silver/main/ts/ui/dialog/Dialogs.ts
@@ -6,10 +6,11 @@ import { Optional, Result } from '@ephox/katamari';
 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();
 
 const hiddenHeader = (title: AlloyParts.ConfiguredPart, close: AlloyParts.ConfiguredPart): AlloySpec => ({
@@ -75,11 +76,11 @@ const pBodyMessage = (message: string, providersBackstage: UiFactoryBackstagePro
         tag: 'div',
         classes: [ 'tox-dialog__body-content' ]
       },
       components: [
         {
-          dom: DomFactory.fromHtml(`<p>${providersBackstage.translate(message)}</p>`)
+          dom: DomFactory.fromHtml(`<p>${HtmlSanitizer.sanitizeHtmlString(providersBackstage.translate(message))}</p>`)
         }
       ]
     }
   ]
 });
diff --git a/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts b/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts
index 618e0fbde9..bd4369172f 100644
--- a/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts
+++ b/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerAlertTest.ts
@@ -182,6 +182,43 @@ describe('headless.tinymce.themes.silver.window.WindowManagerAlertTest', () => {
   it('Check that clicking ok in the dialog makes the dialog go away', () => {
     createAlert('Showing an alert', Fun.noop);
     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();
+  });
 });
diff --git a/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts b/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts
index fcb89bc36f..720a302d27 100644
--- a/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts
+++ b/modules/tinymce/src/themes/silver/test/ts/headless/window/WindowManagerConfirmTest.ts
@@ -193,6 +193,43 @@ describe('headless.tinymce.themes.silver.window.WindowManagerConfirmTest', () =>
   it('Check that clicking close in the dialog makes the dialog go away', () => {
     createConfirm('Showing an confirm', Fun.noop);
     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();
+  });
 });

Vulnerable code:           dom: DomFactory.fromHtml(`<p>${providersBackstage.translate(message)}</p>`)
Fixed code: ### Fixed
- HTML in messages for the `WindowManager.alert` and `WindowManager.confirm` APIs were not properly sanitized. #TINY-3548

import createDompurify from 'dompurify';

export const sanitizeHtmlString = (html: string): string => createDompurify().sanitize(html);
import * as HtmlSanitizer from '../core/HtmlSanitizer';
          dom: DomFactory.fromHtml(`<p>${HtmlSanitizer.sanitizeHtmlString(providersBackstage.translate(message))}</p>`)

  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();
  });

  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();
  });