VYPR
High severity8.7NVD Advisory· Published May 28, 2026· Updated May 28, 2026

CVE-2026-47761

CVE-2026-47761

Description

TinyMCE is an open source rich text editor. Prior to 5.11.1, 7.9.3, and 8.5.1, there is a stored XSS vulnerability in the media plugin. Attackers can inject malicious scripts via crafted data-mce-* attributes, which are executed when content is rendered. Impacts users of TinyMCE with the media plugin enabled. This vulnerability is fixed in 5.11.1, 7.9.3, and 8.5.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A stored XSS vulnerability in TinyMCE's media plugin allows attackers to inject scripts via crafted data-mce-* attributes, fixed in versions 5.11.1, 7.9.3, and 8.5.1.

Vulnerability

TinyMCE versions prior to 5.11.1, 7.9.3, and 8.5.1 contain a stored cross-site scripting (XSS) vulnerability in the media plugin. The bug allows injection of malicious scripts through crafted data-mce-object, data-mce-p-*, data-mce-href, data-mce-src, or data-mce-style attributes, which are not properly sanitized during content rendering [1][2][3]. This affects all configurations where the media plugin is enabled.

Exploitation

An attacker requires the ability to input or edit content in a TinyMCE editor instance that has the media plugin enabled. The attacker crafts HTML with specially designed data-mce-* attributes containing JavaScript payloads. When the content is saved and later rendered in the editor or on a page using the editor's output, the malicious script executes in the context of the user's browser [1][3]. No additional authentication beyond the ability to submit content is needed if the editor is publicly accessible.

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the browser of any user viewing the affected content. This can lead to session hijacking, sensitive data exfiltration, defacement, or other malicious actions at the privilege level of the victim user [3].

Mitigation

The vulnerability is fixed in TinyMCE 5.11.1, 7.9.3, and 8.5.1. Users should upgrade to these or later versions. For TinyMCE 5.x LTS, the fix is available as part of a commercial long-term support contract [3]. No official workaround is available [3].

AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Tinymce/Tinymceinferred2 versions
    <5.11.1 || >=7.0.0,<7.9.3 || >=8.0.0,<8.5.1+ 1 more
    • (no CPE)range: <5.11.1 || >=7.0.0,<7.9.3 || >=8.0.0,<8.5.1
    • (no CPE)range: <5.11.1, <7.9.3, <8.5.1

Patches

6
8da3e85238b0

TINY-14357: Fixed data-mce-object injection (#18)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 8.5.1via llm-release-walk
4 files changed · +145 47
  • .changes/unreleased/tinymce-TINY-14357-2026-04-28.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed media plugin `data-mce-object` injection leading to stored XSS.
    +time: 2026-04-28T14:13:22.407181+02:00
    +custom:
    +    Issue: TINY-14357
    
  • modules/tinymce/src/plugins/media/main/ts/core/FilterContent.ts+79 45 modified
    @@ -1,13 +1,85 @@
    -import { Arr, Obj } from '@ephox/katamari';
    +import { Arr, Obj, Optional, Type } from '@ephox/katamari';
     
     import type Editor from 'tinymce/core/api/Editor';
    -import AstNode from 'tinymce/core/api/html/Node';
    +import type AstNode from 'tinymce/core/api/html/Node';
     
     import * as Nodes from './Nodes';
     import * as Sanitize from './Sanitize';
     
     declare let unescape: any;
     
    +const buildMediaElement = (editor: Editor, node: AstNode) => {
    +  const realElmName = node.attr('data-mce-object') as string;
    +  const element = document.createElement(realElmName);
    +
    +  // Add width/height to everything but audio
    +  if (realElmName !== 'audio') {
    +    const className = node.attr('class');
    +    const firstChild = node.firstChild;
    +
    +    if (className && className.indexOf('mce-preview-object') !== -1 && firstChild) {
    +      const width = firstChild.attr('width');
    +      const height = firstChild.attr('height');
    +
    +      if (Type.isString(width)) {
    +        element.setAttribute('width', width);
    +      }
    +
    +      if (Type.isString(height)) {
    +        element.setAttribute('height', height);
    +      }
    +    } else {
    +      const width = node.attr('width');
    +      const height = node.attr('height');
    +
    +      if (Type.isString(width)) {
    +        element.setAttribute('width', width);
    +      }
    +
    +      if (Type.isString(height)) {
    +        element.setAttribute('height', height);
    +      }
    +    }
    +  }
    +
    +  const style = node.attr('style');
    +  if (Type.isString(style)) {
    +    element.setAttribute('style', style);
    +  }
    +
    +  // Unprefix all placeholder attributes
    +  const attribs = node.attributes ?? [];
    +  let ai = attribs.length;
    +  while (ai--) {
    +    const attrName = attribs[ai].name;
    +
    +    if (attrName.indexOf('data-mce-p-') === 0) {
    +      element.setAttribute(attrName.substr(11), attribs[ai].value);
    +    }
    +  }
    +
    +  // Inject innerhtml
    +  const innerHtml = node.attr('data-mce-html');
    +  if (Type.isString(innerHtml)) {
    +    element.innerHTML = unescape(innerHtml);
    +  } else {
    +    element.innerHTML = '\u00a0';
    +  }
    +
    +  const fragment = Sanitize.parseAndSanitize(editor, element.outerHTML);
    +  const newElement = fragment.getAll(realElmName)[0];
    +
    +  if (Type.isNonNullable(newElement)) {
    +    if (!Type.isString(innerHtml)) {
    +      newElement.empty();
    +    }
    +
    +    return Optional.some(newElement);
    +  } else {
    +    return Optional.none();
    +  }
    +};
    +
     const setup = (editor: Editor): void => {
       editor.on('PreInit', () => {
         const { schema, serializer, parser } = editor;
    @@ -34,56 +106,18 @@ const setup = (editor: Editor): void => {
         parser.addNodeFilter('iframe,video,audio,object,embed', Nodes.placeHolderConverter(editor));
     
         // Replaces placeholder images with real elements for video, object, iframe etc
    -    serializer.addAttributeFilter('data-mce-object', (nodes, name) => {
    +    serializer.addAttributeFilter('data-mce-object', (nodes) => {
           let i = nodes.length;
           while (i--) {
             const node = nodes[i];
             if (!node.parent) {
               continue;
             }
     
    -        const realElmName = node.attr(name) as string;
    -        const realElm = new AstNode(realElmName, 1);
    -
    -        // Add width/height to everything but audio
    -        if (realElmName !== 'audio') {
    -          const className = node.attr('class');
    -          if (className && className.indexOf('mce-preview-object') !== -1 && node.firstChild) {
    -            realElm.attr({
    -              width: node.firstChild.attr('width'),
    -              height: node.firstChild.attr('height')
    -            });
    -          } else {
    -            realElm.attr({
    -              width: node.attr('width'),
    -              height: node.attr('height')
    -            });
    -          }
    -        }
    -
    -        realElm.attr({
    -          style: node.attr('style')
    -        });
    -
    -        // Unprefix all placeholder attributes
    -        const attribs = node.attributes ?? [];
    -        let ai = attribs.length;
    -        while (ai--) {
    -          const attrName = attribs[ai].name;
    -
    -          if (attrName.indexOf('data-mce-p-') === 0) {
    -            realElm.attr(attrName.substr(11), attribs[ai].value);
    -          }
    -        }
    -
    -        // Inject innerhtml
    -        const innerHtml = node.attr('data-mce-html');
    -        if (innerHtml) {
    -          const fragment = Sanitize.parseAndSanitize(editor, realElmName, unescape(innerHtml));
    -          Arr.each(fragment.children(), (child) => realElm.append(child));
    -        }
    -
    -        node.replace(realElm);
    +        buildMediaElement(editor, node).fold(
    +          () => node.remove(),
    +          (realElm) => node.replace(realElm)
    +        );
           }
         });
       });
    
  • modules/tinymce/src/plugins/media/main/ts/core/Sanitize.ts+2 2 modified
    @@ -5,11 +5,11 @@ import * as Options from '../api/Options';
     
     import { Parser } from './Parser';
     
    -const parseAndSanitize = (editor: Editor, context: string, html: string): AstNode => {
    +const parseAndSanitize = (editor: Editor, html: string): AstNode => {
       const getEditorOption = editor.options.get;
       const sanitize = getEditorOption('xss_sanitization');
       const validate = Options.shouldFilterHtml(editor);
    -  return Parser(editor.schema, { sanitize, validate }).parse(html, { context });
    +  return Parser(editor.schema, { sanitize, validate }).parse(html);
     };
     
     export {
    
  • modules/tinymce/src/plugins/media/test/ts/browser/SanitizationTest.ts+58 0 added
    @@ -0,0 +1,58 @@
    +import { describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +import Plugin from 'tinymce/plugins/media/Plugin';
    +
    +describe('browser.tinymce.plugins.media.SanitizationTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    plugins: 'media',
    +    base_url: '/project/tinymce/js/tinymce'
    +  }, [ Plugin ]);
    +
    +  const runTest = (testCase: { inputHtml: string; expectedHtml: string }) => {
    +    const editor = hook.editor();
    +    editor.setContent(testCase.inputHtml);
    +    TinyAssertions.assertContent(editor, testCase.expectedHtml);
    +  };
    +
    +  it('TINY-14357: Event attributes should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="a" data-mce-p-href="#" data-mce-p-onclick="alert(1)">x</p>',
    +    expectedHtml: '<a href="#"></a>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in links should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="a" data-mce-p-href="javascript:alert(1)">x</p>',
    +    expectedHtml: '<a></a>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in images should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="img" data-mce-p-src="javascript:alert(1)">x</p>',
    +    expectedHtml: '<img>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in object should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="object" data-mce-p-data="javascript:alert(1)">x</p>',
    +    expectedHtml: '<object></object>'
    +  }));
    +
    +  it('TINY-14357: scripts as data-mce-object should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="script" data-mce-html="alert(1)">x</p>',
    +    expectedHtml: ''
    +  }));
    +
    +  it('TINY-14357: video with javascript urls should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="video" data-mce-p-src="javascript:alert(1)">x</p>',
    +    expectedHtml: '<video></video>'
    +  }));
    +
    +  it('TINY-14357: video with event attributes should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="video" data-mce-p-src="about:blank" data-mce-p-onclick="alert(1)">x</p>',
    +    expectedHtml: '<video src="about:blank"></video>'
    +  }));
    +
    +  it('TINY-14357: video element with valid properties and inner html should still work', () => runTest({
    +    inputHtml: `<p data-mce-object="video" data-mce-p-src="about:blank" data-mce-html='<a href="#">Unsupported content</a>'>x</p>`,
    +    expectedHtml: '<video src="about:blank"><a href="#">Unsupported content</a></video>'
    +  }));
    +});
    
1d4c86b9c516

TINY-14357: Fixed data-mce-object injection (#19)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 7.9.3via llm-release-walk
4 files changed · +144 46
  • .changes/unreleased/tinymce-TINY-14357-2026-04-28.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed media plugin `data-mce-object` injection leading to stored XSS.
    +time: 2026-04-28T14:13:22.407181+02:00
    +custom:
    +    Issue: TINY-14357
    
  • modules/tinymce/src/plugins/media/main/ts/core/FilterContent.ts+78 44 modified
    @@ -1,4 +1,4 @@
    -import { Arr, Obj } from '@ephox/katamari';
    +import { Arr, Obj, Optional, Type } from '@ephox/katamari';
     
     import Editor from 'tinymce/core/api/Editor';
     import AstNode from 'tinymce/core/api/html/Node';
    @@ -8,6 +8,78 @@ import * as Sanitize from './Sanitize';
     
     declare let unescape: any;
     
    +const buildMediaElement = (editor: Editor, node: AstNode) => {
    +  const realElmName = node.attr('data-mce-object') as string;
    +  const element = document.createElement(realElmName);
    +
    +  // Add width/height to everything but audio
    +  if (realElmName !== 'audio') {
    +    const className = node.attr('class');
    +    const firstChild = node.firstChild;
    +
    +    if (className && className.indexOf('mce-preview-object') !== -1 && firstChild) {
    +      const width = firstChild.attr('width');
    +      const height = firstChild.attr('height');
    +
    +      if (Type.isString(width)) {
    +        element.setAttribute('width', width);
    +      }
    +
    +      if (Type.isString(height)) {
    +        element.setAttribute('height', height);
    +      }
    +    } else {
    +      const width = node.attr('width');
    +      const height = node.attr('height');
    +
    +      if (Type.isString(width)) {
    +        element.setAttribute('width', width);
    +      }
    +
    +      if (Type.isString(height)) {
    +        element.setAttribute('height', height);
    +      }
    +    }
    +  }
    +
    +  const style = node.attr('style');
    +  if (Type.isString(style)) {
    +    element.setAttribute('style', style);
    +  }
    +
    +  // Unprefix all placeholder attributes
    +  const attribs = node.attributes ?? [];
    +  let ai = attribs.length;
    +  while (ai--) {
    +    const attrName = attribs[ai].name;
    +
    +    if (attrName.indexOf('data-mce-p-') === 0) {
    +      element.setAttribute(attrName.substr(11), attribs[ai].value);
    +    }
    +  }
    +
    +  // Inject innerhtml
    +  const innerHtml = node.attr('data-mce-html');
    +  if (Type.isString(innerHtml)) {
    +    element.innerHTML = unescape(innerHtml);
    +  } else {
    +    element.innerHTML = '\u00a0';
    +  }
    +
    +  const fragment = Sanitize.parseAndSanitize(editor, element.outerHTML);
    +  const newElement = fragment.getAll(realElmName)[0];
    +
    +  if (Type.isNonNullable(newElement)) {
    +    if (!Type.isString(innerHtml)) {
    +      newElement.empty();
    +    }
    +
    +    return Optional.some(newElement);
    +  } else {
    +    return Optional.none();
    +  }
    +};
    +
     const setup = (editor: Editor): void => {
       editor.on('PreInit', () => {
         const { schema, serializer, parser } = editor;
    @@ -34,56 +106,18 @@ const setup = (editor: Editor): void => {
         parser.addNodeFilter('iframe,video,audio,object,embed', Nodes.placeHolderConverter(editor));
     
         // Replaces placeholder images with real elements for video, object, iframe etc
    -    serializer.addAttributeFilter('data-mce-object', (nodes, name) => {
    +    serializer.addAttributeFilter('data-mce-object', (nodes) => {
           let i = nodes.length;
           while (i--) {
             const node = nodes[i];
             if (!node.parent) {
               continue;
             }
     
    -        const realElmName = node.attr(name) as string;
    -        const realElm = new AstNode(realElmName, 1);
    -
    -        // Add width/height to everything but audio
    -        if (realElmName !== 'audio') {
    -          const className = node.attr('class');
    -          if (className && className.indexOf('mce-preview-object') !== -1 && node.firstChild) {
    -            realElm.attr({
    -              width: node.firstChild.attr('width'),
    -              height: node.firstChild.attr('height')
    -            });
    -          } else {
    -            realElm.attr({
    -              width: node.attr('width'),
    -              height: node.attr('height')
    -            });
    -          }
    -        }
    -
    -        realElm.attr({
    -          style: node.attr('style')
    -        });
    -
    -        // Unprefix all placeholder attributes
    -        const attribs = node.attributes ?? [];
    -        let ai = attribs.length;
    -        while (ai--) {
    -          const attrName = attribs[ai].name;
    -
    -          if (attrName.indexOf('data-mce-p-') === 0) {
    -            realElm.attr(attrName.substr(11), attribs[ai].value);
    -          }
    -        }
    -
    -        // Inject innerhtml
    -        const innerHtml = node.attr('data-mce-html');
    -        if (innerHtml) {
    -          const fragment = Sanitize.parseAndSanitize(editor, realElmName, unescape(innerHtml));
    -          Arr.each(fragment.children(), (child) => realElm.append(child));
    -        }
    -
    -        node.replace(realElm);
    +        buildMediaElement(editor, node).fold(
    +          () => node.remove(),
    +          (realElm) => node.replace(realElm)
    +        );
           }
         });
       });
    
  • modules/tinymce/src/plugins/media/main/ts/core/Sanitize.ts+2 2 modified
    @@ -4,11 +4,11 @@ import AstNode from 'tinymce/core/api/html/Node';
     import * as Options from '../api/Options';
     import { Parser } from './Parser';
     
    -const parseAndSanitize = (editor: Editor, context: string, html: string): AstNode => {
    +const parseAndSanitize = (editor: Editor, html: string): AstNode => {
       const getEditorOption = editor.options.get;
       const sanitize = getEditorOption('xss_sanitization');
       const validate = Options.shouldFilterHtml(editor);
    -  return Parser(editor.schema, { sanitize, validate }).parse(html, { context });
    +  return Parser(editor.schema, { sanitize, validate }).parse(html);
     };
     
     export {
    
  • modules/tinymce/src/plugins/media/test/ts/browser/SanitizationTest.ts+58 0 added
    @@ -0,0 +1,58 @@
    +import { describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +import Plugin from 'tinymce/plugins/media/Plugin';
    +
    +describe('browser.tinymce.plugins.media.SanitizationTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    plugins: 'media',
    +    base_url: '/project/tinymce/js/tinymce'
    +  }, [ Plugin ]);
    +
    +  const runTest = (testCase: { inputHtml: string; expectedHtml: string }) => {
    +    const editor = hook.editor();
    +    editor.setContent(testCase.inputHtml);
    +    TinyAssertions.assertContent(editor, testCase.expectedHtml);
    +  };
    +
    +  it('TINY-14357: Event attributes should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="a" data-mce-p-href="#" data-mce-p-onclick="alert(1)">x</p>',
    +    expectedHtml: '<a href="#"></a>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in links should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="a" data-mce-p-href="javascript:alert(1)">x</p>',
    +    expectedHtml: '<a></a>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in images should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="img" data-mce-p-src="javascript:alert(1)">x</p>',
    +    expectedHtml: '<img>'
    +  }));
    +
    +  it('TINY-14357: javascript urls in object should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="object" data-mce-p-data="javascript:alert(1)">x</p>',
    +    expectedHtml: '<object></object>'
    +  }));
    +
    +  it('TINY-14357: scripts as data-mce-object should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="script" data-mce-html="alert(1)">x</p>',
    +    expectedHtml: ''
    +  }));
    +
    +  it('TINY-14357: video with javascript urls should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="video" data-mce-p-src="javascript:alert(1)">x</p>',
    +    expectedHtml: '<video></video>'
    +  }));
    +
    +  it('TINY-14357: video with event attributes should not be allowed', () => runTest({
    +    inputHtml: '<p data-mce-object="video" data-mce-p-src="about:blank" data-mce-p-onclick="alert(1)">x</p>',
    +    expectedHtml: '<video src="about:blank"></video>'
    +  }));
    +
    +  it('TINY-14357: video element with valid properties and inner html should still work', () => runTest({
    +    inputHtml: `<p data-mce-object="video" data-mce-p-src="about:blank" data-mce-html='<a href="#">Unsupported content</a>'>x</p>`,
    +    expectedHtml: '<video src="about:blank"><a href="#">Unsupported content</a></video>'
    +  }));
    +});
    
0029dada0303

TINY-14333: Fixed stored XSS vulnerability through `data-mce-` prefixed `src`, `href`, `style` attributes (#23)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 7.9.3via llm-release-walk
4 files changed · +102 0
  • .changes/unreleased/tinymce-TINY-14333-2026-04-29.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed stored XSS vulnerability through `data-mce-` prefixed `src`, `href`, `style` attributes.
    +time: 2026-04-29T11:07:42.702682+02:00
    +custom:
    +    Issue: TINY-14333
    
  • modules/tinymce/src/core/main/ts/init/InitContentBody.ts+6 0 modified
    @@ -142,6 +142,12 @@ const mkSerializerSettings = (editor: Editor): DomSerializerSettings => {
     const createParser = (editor: Editor): DomParser => {
       const parser = DomParser(mkParserSettings(editor), editor.schema);
     
    +  parser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', (nodes, name) => {
    +    for (let i = 0; i < nodes.length; i++) {
    +      nodes[i].attr(name, null);
    +    }
    +  });
    +
       // Convert src and href into data-mce-src, data-mce-href and data-mce-style
       parser.addAttributeFilter('src,href,style,tabindex', (nodes, name) => {
         const dom = editor.dom;
    
  • modules/tinymce/src/core/test/ts/browser/content/EditorContentRetainAttributesTest.ts+41 0 added
    @@ -0,0 +1,41 @@
    +import { describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.content.EditorContentRetailAttributesTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce'
    +  }, []);
    +
    +  const testContentRetainAttributes = (testCase: { input: string; expected: string }) => {
    +    const editor = hook.editor();
    +
    +    editor.setContent(testCase.input);
    +    TinyAssertions.assertContent(editor, testCase.expected);
    +
    +    editor.setContent('');
    +    editor.insertContent(testCase.input);
    +    TinyAssertions.assertContent(editor, testCase.expected);
    +  };
    +
    +  it('TINY-14333: Should support valid style, src and href', () => testContentRetainAttributes({
    +    input: '<p><span style="color: red;">Red</span> <a href="#">link</a> <img src="about:blank"></p>',
    +    expected: '<p><span style="color: red;">Red</span> <a href="#">link</a> <img src="about:blank"></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-style attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: `<p><span data-mce-style="color: url('javascript:alert(1)');" style="color: red;">Red</span></p>`,
    +    expected: '<p><span style="color: red;">Red</span></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-src attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: '<p><img data-mce-src="javascript:alert(1)" src="about:blank"></p>',
    +    expected: '<p><img src="about:blank"></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-href attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: '<p><a data-mce-href="javascript:alert(1)" href="about:blank">link</a></p>',
    +    expected: '<p><a href="about:blank">link</a></p>'
    +  }));
    +});
    
  • modules/tinymce/src/core/test/ts/browser/paste/PasteRetainAttributesTest.ts+49 0 added
    @@ -0,0 +1,49 @@
    +import { Clipboard } from '@ephox/agar';
    +import { beforeEach, context, describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyDom, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.paste.PasteRetainAttributesTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce',
    +    indent: false,
    +    paste_webkit_styles: 'all'
    +  }, []);
    +
    +  context('XSS paste tests', () => {
    +    const payload = [
    +      '<p>',
    +      '<a data-mce-href="javascript:alert(1)" href="#">Click here</a>',
    +      '<img data-mce-src="javascript:alert(1)" src="about:blank">',
    +      `<span data-mce-style="background: url('javascript:alert(1)');" style="color: red;">Red</span>`,
    +      '</p>'
    +    ].join('');
    +    const expectedOutput = [
    +      '<p>',
    +      '<a href="#">Click here</a>',
    +      '<img src="about:blank">',
    +      `<span style="color: red;">Red</span>`,
    +      '</p>'
    +    ].join('');
    +
    +    beforeEach(() => {
    +      const editor = hook.editor();
    +      editor.setContent('');
    +    });
    +
    +    it('TINY-14333: Paste XSS payload using mceInsertClipboardContent command', () => {
    +      const editor = hook.editor();
    +
    +      editor.execCommand('mceInsertClipboardContent', false, { html: payload });
    +      TinyAssertions.assertContent(editor, expectedOutput);
    +    });
    +
    +    it('TINY-14333: Paste XSS payload using paste event', () => {
    +      const editor = hook.editor();
    +
    +      Clipboard.pasteItems(TinyDom.body(editor), { 'text/html': payload });
    +      TinyAssertions.assertContent(editor, expectedOutput);
    +    });
    +  });
    +});
    
a7d77018dd27

TINY-14353: Fixed stored XSS vulnerability through `mce:protected` comments (#24)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 7.9.3via llm-release-walk
6 files changed · +131 31
  • .changes/unreleased/tinymce-TINY-14353-2026-04-29.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed stored XSS vulnerability through `mce:protected` comments.
    +time: 2026-04-29T09:58:39.925795+02:00
    +custom:
    +    Issue: TINY-14353
    
  • modules/tinymce/src/core/main/ts/dom/DomSerializerFilters.ts+0 5 modified
    @@ -129,11 +129,6 @@ const register = (htmlParser: DomParser, settings: DomSerializerSettings, dom: D
             node.name = '#cdata';
             node.type = 4;
             node.value = dom.decode(value.replace(/^\[CDATA\[|\]\]$/g, ''));
    -      } else if (value?.indexOf('mce:protected ') === 0) {
    -        node.name = '#text';
    -        node.type = 3;
    -        node.raw = true;
    -        node.value = unescape(value).substr(14);
           }
         }
       });
    
  • modules/tinymce/src/core/main/ts/html/ProtectedFilter.ts+45 0 added
    @@ -0,0 +1,45 @@
    +import { Arr } from '@ephox/katamari';
    +
    +import type Editor from '../api/Editor';
    +
    +const setupInputFiltering = (editor: Editor, protect: RegExp[]): void => {
    +  editor.on('BeforeSetContent', (e) => {
    +    Arr.each(protect, (pattern) => {
    +      e.content = e.content.replace(pattern, (str) => {
    +        return '<!--mce:protected ' + escape(str) + '-->';
    +      });
    +    });
    +  });
    +};
    +
    +const setupOutputFiltering = (editor: Editor, protect: RegExp[]): void => {
    +  editor.serializer.addNodeFilter('#comment', (nodes) => {
    +    let i = nodes.length;
    +    while (i--) {
    +      const node = nodes[i];
    +      const value = node.value;
    +
    +      if (value?.indexOf('mce:protected ') === 0) {
    +        const protectedHtml = unescape(value).substr(14);
    +        const valid = Arr.exists(protect, (pattern) => {
    +          const matches = protectedHtml.match(pattern);
    +          return matches !== null && matches[0].length === protectedHtml.length;
    +        });
    +
    +        if (valid) {
    +          node.name = '#text';
    +          node.type = 3;
    +          node.raw = true;
    +          node.value = protectedHtml;
    +        } else {
    +          node.remove();
    +        }
    +      }
    +    }
    +  });
    +};
    +
    +export const registerProtectedHtmlFilters = (editor: Editor, protect: RegExp[]): void => {
    +  setupInputFiltering(editor, protect);
    +  setupOutputFiltering(editor, protect);
    +};
    
  • modules/tinymce/src/core/main/ts/init/InitContentBody.ts+2 8 modified
    @@ -26,6 +26,7 @@ import * as NodeType from '../dom/NodeType';
     import * as TouchEvents from '../events/TouchEvents';
     import * as ForceBlocks from '../ForceBlocks';
     import * as NonEditableFilter from '../html/NonEditableFilter';
    +import * as ProtectedFilter from '../html/ProtectedFilter';
     import * as KeyboardOverrides from '../keyboard/KeyboardOverrides';
     import * as Disabled from '../mode/Disabled';
     import { NodeChange } from '../NodeChange';
    @@ -40,7 +41,6 @@ import Quirks from '../util/Quirks';
     import * as ContentCss from './ContentCss';
     import * as LicenseKeyValidation from './LicenseKeyValidation';
     
    -declare const escape: any;
     declare let tinymce: TinyMCE;
     
     const DOM = DOMUtils.DOM;
    @@ -362,13 +362,7 @@ const preInit = (editor: Editor) => {
     
       const protect = Options.getProtect(editor);
       if (protect) {
    -    editor.on('BeforeSetContent', (e) => {
    -      Tools.each(protect, (pattern) => {
    -        e.content = e.content.replace(pattern, (str) => {
    -          return '<!--mce:protected ' + escape(str) + '-->';
    -        });
    -      });
    -    });
    +    ProtectedFilter.registerProtectedHtmlFilters(editor, protect);
       }
     
       editor.on('SetContent', () => {
    
  • modules/tinymce/src/core/test/ts/browser/content/EditorContentProtectOptionTest.ts+78 0 added
    @@ -0,0 +1,78 @@
    +import { context, describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.content.EditorContentProtectOptionTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce',
    +    protect: [
    +      /\<\/?(if|endif)\>/g,
    +      /\<xsl\:[^>]+\>/g,
    +      /<\?php.*?\?>/g
    +    ],
    +    indent: false
    +  });
    +
    +  context('Valid protect content', () => {
    +    const testValidProtect = (testCase: { input: string; expected: string }) => {
    +      const editor = hook.editor();
    +
    +      editor.setContent(testCase.input);
    +
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +    };
    +
    +    it('TINY-14353: Protect tags', () => testValidProtect({
    +      input: '<p>A</p><if><p>B</p></if><p>C</p>',
    +      expected: '<p>A</p><if><p>B</p></if><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect xsl', () => testValidProtect({
    +      input: '<p>A</p><xsl:foo><p>B</p><p>C</p>',
    +      expected: '<p>A</p><xsl:foo><p>B</p><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect php', () => testValidProtect({
    +      input: '<p>A</p><?php echo "B"; ?><p>C</p>',
    +      expected: '<p>A</p><?php echo "B"; ?><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect multiple tags', () => testValidProtect({
    +      input: '<p>A</p><if><p>B</p></if><xsl:foo><p>C</p><?php echo "D"; ?><p>E</p>',
    +      expected: '<p>A</p><if><p>B</p></if><xsl:foo><p>C</p><?php echo "D"; ?><p>E</p>'
    +    }));
    +  });
    +
    +  context('Raw comment injection', () => {
    +    const testInnerContent = (testCase: { innerContent: string; expected: string }) => {
    +      const editor = hook.editor();
    +
    +      editor.setContent(`<!--mce:protected ${escape(testCase.innerContent)}-->`);
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +
    +      editor.setContent(`<!--mce:protected ${escape(testCase.innerContent)}-->`, { format: 'raw' });
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +    };
    +
    +    it('TINY-14353: Script payload should not be allowed', () => testInnerContent({
    +      innerContent: '<script>alert(1)</script>',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Img with attribute payload should not be allowed', () => testInnerContent({
    +      innerContent: '<img src="#" onerror="alert(1)">',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Script payload with partial matching protect should not be allowed', () => testInnerContent({
    +      innerContent: '<if><script>alert(1)</script>',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Valid content', () => testInnerContent({
    +      innerContent: '<if>',
    +      expected: '<if>'
    +    }));
    +  });
    +});
    
  • modules/tinymce/src/core/test/ts/browser/dom/SerializerTest.ts+0 18 modified
    @@ -9,8 +9,6 @@ import * as Zwsp from 'tinymce/core/text/Zwsp';
     
     import * as ViewBlock from '../../module/test/ViewBlock';
     
    -declare const escape: any;
    -
     describe('browser.tinymce.core.dom.SerializerTest', () => {
       const DOM = DOMUtils.DOM;
       const viewBlock = ViewBlock.bddSetup();
    @@ -655,21 +653,6 @@ describe('browser.tinymce.core.dom.SerializerTest', () => {
         assert.equal(ser.serialize(getTestElement()), '<pre>  </pre>');
       });
     
    -  it('Protected blocks', () => {
    -    const ser = DomSerializer({ fix_list_elements: true });
    -
    -    ser.setRules('noscript[test]');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript test="test"><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript test="test"><br></noscript>');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript><br></noscript>');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript><!-- text --><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript><!-- text --><br></noscript>');
    -  });
    -
       it('CDATA', () => {
         const ser = DomSerializer({ fix_list_elements: true, preserve_cdata: true });
         ser.setRules('span');
    @@ -936,4 +919,3 @@ describe('browser.tinymce.core.dom.SerializerTest', () => {
           'Should remove br');
       });
     });
    -
    
3507b58a55fa

TINY-14353: Fixed stored XSS vulnerability through `mce:protected` comments (#21)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 8.5.1via llm-release-walk
6 files changed · +132 31
  • .changes/unreleased/tinymce-TINY-14353-2026-04-29.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed stored XSS vulnerability through `mce:protected` comments.
    +time: 2026-04-29T09:58:39.925795+02:00
    +custom:
    +    Issue: TINY-14353
    
  • modules/tinymce/src/core/main/ts/dom/DomSerializerFilters.ts+1 6 modified
    @@ -119,7 +119,7 @@ const register = (htmlParser: DomParser, settings: DomSerializerSettings, dom: D
         }
       });
     
    -  // Convert comments to cdata and handle protected comments
    +  // Convert comments to cdata
       htmlParser.addNodeFilter('#comment', (nodes) => {
         let i = nodes.length;
         while (i--) {
    @@ -130,11 +130,6 @@ const register = (htmlParser: DomParser, settings: DomSerializerSettings, dom: D
             node.name = '#cdata';
             node.type = 4;
             node.value = dom.decode(value.replace(/^\[CDATA\[|\]\]$/g, ''));
    -      } else if (value?.indexOf('mce:protected ') === 0) {
    -        node.name = '#text';
    -        node.type = 3;
    -        node.raw = true;
    -        node.value = unescape(value).substr(14);
           }
         }
       });
    
  • modules/tinymce/src/core/main/ts/html/ProtectedFilter.ts+45 0 added
    @@ -0,0 +1,45 @@
    +import { Arr } from '@ephox/katamari';
    +
    +import type Editor from '../api/Editor';
    +
    +const setupInputFiltering = (editor: Editor, protect: RegExp[]): void => {
    +  editor.on('BeforeSetContent', (e) => {
    +    Arr.each(protect, (pattern) => {
    +      e.content = e.content.replace(pattern, (str) => {
    +        return '<!--mce:protected ' + escape(str) + '-->';
    +      });
    +    });
    +  });
    +};
    +
    +const setupOutputFiltering = (editor: Editor, protect: RegExp[]): void => {
    +  editor.serializer.addNodeFilter('#comment', (nodes) => {
    +    let i = nodes.length;
    +    while (i--) {
    +      const node = nodes[i];
    +      const value = node.value;
    +
    +      if (value?.indexOf('mce:protected ') === 0) {
    +        const protectedHtml = unescape(value).substr(14);
    +        const valid = Arr.exists(protect, (pattern) => {
    +          const matches = protectedHtml.match(pattern);
    +          return matches !== null && matches[0].length === protectedHtml.length;
    +        });
    +
    +        if (valid) {
    +          node.name = '#text';
    +          node.type = 3;
    +          node.raw = true;
    +          node.value = protectedHtml;
    +        } else {
    +          node.remove();
    +        }
    +      }
    +    }
    +  });
    +};
    +
    +export const registerProtectedHtmlFilters = (editor: Editor, protect: RegExp[]): void => {
    +  setupInputFiltering(editor, protect);
    +  setupOutputFiltering(editor, protect);
    +};
    
  • modules/tinymce/src/core/main/ts/init/InitContentBody.ts+2 8 modified
    @@ -26,6 +26,7 @@ import * as NodeType from '../dom/NodeType';
     import * as TouchEvents from '../events/TouchEvents';
     import * as ForceBlocks from '../ForceBlocks';
     import * as NonEditableFilter from '../html/NonEditableFilter';
    +import * as ProtectedFilter from '../html/ProtectedFilter';
     import * as KeyboardOverrides from '../keyboard/KeyboardOverrides';
     import * as Lists from '../lists/Lists';
     import * as Disabled from '../mode/Disabled';
    @@ -41,7 +42,6 @@ import Quirks from '../util/Quirks';
     
     import * as InitComponents from './InitComponents';
     
    -declare const escape: any;
     declare let tinymce: TinyMCE;
     
     const DOM = DOMUtils.DOM;
    @@ -369,13 +369,7 @@ const preInit = (editor: Editor) => {
     
       const protect = Options.getProtect(editor);
       if (protect) {
    -    editor.on('BeforeSetContent', (e) => {
    -      Tools.each(protect, (pattern) => {
    -        e.content = e.content.replace(pattern, (str) => {
    -          return '<!--mce:protected ' + escape(str) + '-->';
    -        });
    -      });
    -    });
    +    ProtectedFilter.registerProtectedHtmlFilters(editor, protect);
       }
     
       editor.on('SetContent', () => {
    
  • modules/tinymce/src/core/test/ts/browser/content/EditorContentProtectOptionTest.ts+78 0 added
    @@ -0,0 +1,78 @@
    +import { context, describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.content.EditorContentProtectOptionTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce',
    +    protect: [
    +      /\<\/?(if|endif)\>/g,
    +      /\<xsl\:[^>]+\>/g,
    +      /<\?php.*?\?>/g
    +    ],
    +    indent: false
    +  });
    +
    +  context('Valid protect content', () => {
    +    const testValidProtect = (testCase: { input: string; expected: string }) => {
    +      const editor = hook.editor();
    +
    +      editor.setContent(testCase.input);
    +
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +    };
    +
    +    it('TINY-14353: Protect tags', () => testValidProtect({
    +      input: '<p>A</p><if><p>B</p></if><p>C</p>',
    +      expected: '<p>A</p><if><p>B</p></if><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect xsl', () => testValidProtect({
    +      input: '<p>A</p><xsl:foo><p>B</p><p>C</p>',
    +      expected: '<p>A</p><xsl:foo><p>B</p><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect php', () => testValidProtect({
    +      input: '<p>A</p><?php echo "B"; ?><p>C</p>',
    +      expected: '<p>A</p><?php echo "B"; ?><p>C</p>'
    +    }));
    +
    +    it('TINY-14353: Protect multiple tags', () => testValidProtect({
    +      input: '<p>A</p><if><p>B</p></if><xsl:foo><p>C</p><?php echo "D"; ?><p>E</p>',
    +      expected: '<p>A</p><if><p>B</p></if><xsl:foo><p>C</p><?php echo "D"; ?><p>E</p>'
    +    }));
    +  });
    +
    +  context('Raw comment injection', () => {
    +    const testInnerContent = (testCase: { innerContent: string; expected: string }) => {
    +      const editor = hook.editor();
    +
    +      editor.setContent(`<!--mce:protected ${escape(testCase.innerContent)}-->`);
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +
    +      editor.setContent(`<!--mce:protected ${escape(testCase.innerContent)}-->`, { format: 'raw' });
    +      TinyAssertions.assertContent(editor, testCase.expected);
    +    };
    +
    +    it('TINY-14353: Script payload should not be allowed', () => testInnerContent({
    +      innerContent: '<script>alert(1)</script>',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Img with attribute payload should not be allowed', () => testInnerContent({
    +      innerContent: '<img src="#" onerror="alert(1)">',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Script payload with partial matching protect should not be allowed', () => testInnerContent({
    +      innerContent: '<if><script>alert(1)</script>',
    +      expected: ''
    +    }));
    +
    +    it('TINY-14353: Valid content', () => testInnerContent({
    +      innerContent: '<if>',
    +      expected: '<if>'
    +    }));
    +  });
    +});
    
  • modules/tinymce/src/core/test/ts/browser/dom/SerializerTest.ts+0 17 modified
    @@ -9,8 +9,6 @@ import * as Zwsp from 'tinymce/core/text/Zwsp';
     
     import * as ViewBlock from '../../module/test/ViewBlock';
     
    -declare const escape: any;
    -
     describe('browser.tinymce.core.dom.SerializerTest', () => {
       const DOM = DOMUtils.DOM;
       const viewBlock = ViewBlock.bddSetup();
    @@ -608,21 +606,6 @@ describe('browser.tinymce.core.dom.SerializerTest', () => {
         assert.equal(ser.serialize(getTestElement()), '<script>/* <!-- */\nvar hi = \"hello\";\n/*-->*/</script>');
       });
     
    -  it('Protected blocks', () => {
    -    const ser = DomSerializer({ fix_list_elements: true });
    -
    -    ser.setRules('noscript[test]');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript test="test"><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript test="test"><br></noscript>');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript><br></noscript>');
    -
    -    setTestHtml('<!--mce:protected ' + escape('<noscript><!-- text --><br></noscript>') + '-->');
    -    assert.equal(ser.serialize(getTestElement()), '<noscript><!-- text --><br></noscript>');
    -  });
    -
       it('Style with whitespace at beginning with element_format: xhtml', () => {
         const ser = DomSerializer({ fix_list_elements: true, valid_children: '+body[style]', element_format: 'xhtml' });
         ser.setRules('style');
    
19f56a74f53a

TINY-14333: Fixed stored XSS vulnerability through `data-mce-` prefixed `src`, `href`, `style` attributes (#22)

https://github.com/tinymce/tinymcespockeMay 5, 2026Fixed in 8.5.1via llm-release-walk
4 files changed · +102 0
  • .changes/unreleased/tinymce-TINY-14333-2026-04-29.yaml+6 0 added
    @@ -0,0 +1,6 @@
    +project: tinymce
    +kind: Security
    +body: Fixed stored XSS vulnerability through `data-mce-` prefixed `src`, `href`, `style` attributes.
    +time: 2026-04-29T11:07:42.702682+02:00
    +custom:
    +    Issue: TINY-14333
    
  • modules/tinymce/src/core/main/ts/init/InitContentBody.ts+6 0 modified
    @@ -143,6 +143,12 @@ const mkSerializerSettings = (editor: Editor): DomSerializerSettings => {
     const createParser = (editor: Editor): DomParser => {
       const parser = DomParser(mkParserSettings(editor), editor.schema);
     
    +  parser.addAttributeFilter('data-mce-src,data-mce-href,data-mce-style', (nodes, name) => {
    +    for (let i = 0; i < nodes.length; i++) {
    +      nodes[i].attr(name, null);
    +    }
    +  });
    +
       // Convert src and href into data-mce-src, data-mce-href and data-mce-style
       parser.addAttributeFilter('src,href,style,tabindex', (nodes, name) => {
         const dom = editor.dom;
    
  • modules/tinymce/src/core/test/ts/browser/content/EditorContentRetainAttributesTest.ts+41 0 added
    @@ -0,0 +1,41 @@
    +import { describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.content.EditorContentRetailAttributesTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce'
    +  }, []);
    +
    +  const testContentRetainAttributes = (testCase: { input: string; expected: string }) => {
    +    const editor = hook.editor();
    +
    +    editor.setContent(testCase.input);
    +    TinyAssertions.assertContent(editor, testCase.expected);
    +
    +    editor.setContent('');
    +    editor.insertContent(testCase.input);
    +    TinyAssertions.assertContent(editor, testCase.expected);
    +  };
    +
    +  it('TINY-14333: Should support valid style, src and href', () => testContentRetainAttributes({
    +    input: '<p><span style="color: red;">Red</span> <a href="#">link</a> <img src="about:blank"></p>',
    +    expected: '<p><span style="color: red;">Red</span> <a href="#">link</a> <img src="about:blank"></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-style attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: `<p><span data-mce-style="color: url('javascript:alert(1)');" style="color: red;">Red</span></p>`,
    +    expected: '<p><span style="color: red;">Red</span></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-src attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: '<p><img data-mce-src="javascript:alert(1)" src="about:blank"></p>',
    +    expected: '<p><img src="about:blank"></p>'
    +  }));
    +
    +  it('TINY-14333: data-mce-href attribute in input should be ignored', () => testContentRetainAttributes({
    +    input: '<p><a data-mce-href="javascript:alert(1)" href="about:blank">link</a></p>',
    +    expected: '<p><a href="about:blank">link</a></p>'
    +  }));
    +});
    
  • modules/tinymce/src/core/test/ts/browser/paste/PasteRetainAttributesTest.ts+49 0 added
    @@ -0,0 +1,49 @@
    +import { Clipboard } from '@ephox/agar';
    +import { beforeEach, context, describe, it } from '@ephox/bedrock-client';
    +import { TinyAssertions, TinyDom, TinyHooks } from '@ephox/wrap-mcagar';
    +
    +import type Editor from 'tinymce/core/api/Editor';
    +
    +describe('browser.tinymce.core.paste.PasteRetainAttributesTest', () => {
    +  const hook = TinyHooks.bddSetupLight<Editor>({
    +    base_url: '/project/tinymce/js/tinymce',
    +    indent: false,
    +    paste_webkit_styles: 'all'
    +  }, []);
    +
    +  context('XSS paste tests', () => {
    +    const payload = [
    +      '<p>',
    +      '<a data-mce-href="javascript:alert(1)" href="#">Click here</a>',
    +      '<img data-mce-src="javascript:alert(1)" src="about:blank">',
    +      `<span data-mce-style="background: url('javascript:alert(1)');" style="color: red;">Red</span>`,
    +      '</p>'
    +    ].join('');
    +    const expectedOutput = [
    +      '<p>',
    +      '<a href="#">Click here</a>',
    +      '<img src="about:blank">',
    +      `<span style="color: red;">Red</span>`,
    +      '</p>'
    +    ].join('');
    +
    +    beforeEach(() => {
    +      const editor = hook.editor();
    +      editor.setContent('');
    +    });
    +
    +    it('TINY-14333: Paste XSS payload using mceInsertClipboardContent command', () => {
    +      const editor = hook.editor();
    +
    +      editor.execCommand('mceInsertClipboardContent', false, { html: payload });
    +      TinyAssertions.assertContent(editor, expectedOutput);
    +    });
    +
    +    it('TINY-14333: Paste XSS payload using paste event', () => {
    +      const editor = hook.editor();
    +
    +      Clipboard.pasteItems(TinyDom.body(editor), { 'text/html': payload });
    +      TinyAssertions.assertContent(editor, expectedOutput);
    +    });
    +  });
    +});
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

3

News mentions

0

No linked articles in our index yet.