Skip to content

Commit

Permalink
MDL-78714 editor_tiny: Add xss_sanitize option to TinyMCE
Browse files Browse the repository at this point in the history
To address a potential data loss issue, a feature introduced in TinyMCE
6.4.0 to disable client-side XSS sanitisation must be backported.
  • Loading branch information
andrewnicols committed Aug 9, 2023
1 parent 737f657 commit e8eb894
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 44 deletions.
7 changes: 6 additions & 1 deletion lib/editor/tiny/js/tinymce/plugins/media/plugin.js
Expand Up @@ -1044,8 +1044,13 @@
};

const parseAndSanitize = (editor, context, html) => {
const getEditorOption = editor.options.get;
const sanitize = getEditorOption('xss_sanitization');
const validate = shouldFilterHtml(editor);
return Parser(editor.schema, { validate }).parse(html, { context });
return Parser(editor.schema, {
sanitize,
validate
}).parse(html, { context });
};

const setup$1 = editor => {
Expand Down
2 changes: 1 addition & 1 deletion lib/editor/tiny/js/tinymce/plugins/media/plugin.min.js

Large diffs are not rendered by default.

48 changes: 41 additions & 7 deletions lib/editor/tiny/js/tinymce/readme_moodle.md
@@ -1,5 +1,17 @@
# This is a description for the TinyMCE 6 library integration with Moodle.

Please note that we have a clone of the official TinyMCE repository which contains the working build and branch for each release. This ensures build repeatability and gives us the ability to patch stable versions of Moodle for security fixes where relevant.

The Moodle `master` branch is named as the upcoming STABLE branch name, for example during the development of Moodle 4.2.0, the upcoming STABLE branch name will be MOODLE_402_STABLE.

## Patches included in this release

- MDL-78714: Add support for disabling XSS Sanitisation (TINY-9600)
- MDL-77470: Fix for CVE-2022-23494

Please note: TinyMCE issue numbers are related to bugs in their private issue
tracker. See git history of their repository for relevant information.

## Upgrade procedure for TinyMCE Editor

1. Store an environment variable to the Tiny directory in the Moodle repository (the current directory).
Expand All @@ -25,21 +37,43 @@
sed -i 's/"target.*es.*",/"target": "es2020",/' tsconfig.shared.json
```

4. Rebuild TinyMCE
4. Install dependencies

```
yarn
```

5. Check in the base changes

```
git commit -m 'MDL: Add build configuration'
```

6. Apply any necessary security patches.
7. Rebuild TinyMCE

```
yarn
yarn build
```

8. Remove the old TinyMCE configuration and replace it with the newly built version.

```
yarn
yarn build
rm -rf "${MOODLEDIR}/js"
cp -r modules/tinymce/js "${MOODLEDIR}/js"
```

5. Remove the old TinyMCE configuration and replace it with the newly built version.
9. Push the build to MoodleHQ for future change support

```
rm -rf "${MOODLEDIR}/js"
cp -r modules/tinymce/js "${MOODLEDIR}/js"
# Tag the next Moodle version.
git tag v4.2.0
git remote add moodlehq --tags
git push moodlehq MOODLE_402_STABLE
```

6. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php
10. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php

## Update procedure for included TinyMCE translations

Expand Down
20 changes: 8 additions & 12 deletions lib/editor/tiny/js/tinymce/themes/silver/theme.js
Expand Up @@ -26917,7 +26917,6 @@
var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI;
value = name === 'value' ? attr.value : stringTrim(attr.value);
lcName = transformCaseFunc(name);
var initValue = value;
hookEvent.attrName = lcName;
hookEvent.attrValue = value;
hookEvent.keepAttr = true;
Expand All @@ -26927,8 +26926,8 @@
if (hookEvent.forceKeepAttr) {
continue;
}
_removeAttribute(name, currentNode);
if (!hookEvent.keepAttr) {
_removeAttribute(name, currentNode);
continue;
}
if (regExpTest(/\/>/i, value)) {
Expand All @@ -26941,19 +26940,16 @@
}
var lcTag = transformCaseFunc(currentNode.nodeName);
if (!_isValidAttribute(lcTag, lcName, value)) {
_removeAttribute(name, currentNode);
continue;
}
if (value !== initValue) {
try {
if (namespaceURI) {
currentNode.setAttributeNS(namespaceURI, name, value);
} else {
currentNode.setAttribute(name, value);
}
} catch (_) {
_removeAttribute(name, currentNode);
try {
if (namespaceURI) {
currentNode.setAttributeNS(namespaceURI, name, value);
} else {
currentNode.setAttribute(name, value);
}
arrayPop(DOMPurify.removed);
} catch (_) {
}
}
_executeHook('afterSanitizeAttributes', currentNode, null);
Expand Down
2 changes: 1 addition & 1 deletion lib/editor/tiny/js/tinymce/themes/silver/theme.min.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions lib/editor/tiny/js/tinymce/tinymce.d.ts
Expand Up @@ -1281,6 +1281,7 @@ interface DomParserSettings {
preserve_cdata?: boolean;
remove_trailing_brs?: boolean;
root_name?: string;
sanitize?: boolean;
validate?: boolean;
inline_styles?: boolean;
blob_cache?: BlobCache;
Expand Down Expand Up @@ -1870,6 +1871,7 @@ interface BaseEditorOptions {
visual_anchor_class?: string;
visual_table_class?: string;
width?: number | string;
xss_sanitization?: boolean;
disable_nodechange?: boolean;
forced_plugins?: string | string[];
plugin_base_urls?: Record<string, string>;
Expand Down Expand Up @@ -1954,6 +1956,7 @@ interface EditorOptions extends NormalizedEditorOptions {
visual_anchor_class: string;
visual_table_class: string;
width: number | string;
xss_sanitization: boolean;
}
declare type StyleMap = Record<string, string | number>;
interface StylesSettings {
Expand Down
48 changes: 27 additions & 21 deletions lib/editor/tiny/js/tinymce/tinymce.js
Expand Up @@ -7046,6 +7046,10 @@
processor: 'boolean',
default: true
});
registerOption('xss_sanitization', {
processor: 'boolean',
default: true
});
editor.on('ScriptsLoaded', () => {
registerOption('directionality', {
processor: 'string',
Expand Down Expand Up @@ -7145,6 +7149,7 @@
const getEditableClass = option('editable_class');
const getNonEditableRegExps = option('noneditable_regexp');
const shouldPreserveCData = option('preserve_cdata');
const shouldSanitizeXss = option('xss_sanitization');
const hasTextPatternsLookup = editor => editor.options.isSet('text_patterns_lookup');
const getFontStyleValues = editor => Tools.explode(editor.options.get('font_size_style_values'));
const getFontSizeClasses = editor => Tools.explode(editor.options.get('font_size_classes'));
Expand Down Expand Up @@ -15463,7 +15468,6 @@
var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI;
value = name === 'value' ? attr.value : stringTrim(attr.value);
lcName = transformCaseFunc(name);
var initValue = value;
hookEvent.attrName = lcName;
hookEvent.attrValue = value;
hookEvent.keepAttr = true;
Expand All @@ -15473,8 +15477,8 @@
if (hookEvent.forceKeepAttr) {
continue;
}
_removeAttribute(name, currentNode);
if (!hookEvent.keepAttr) {
_removeAttribute(name, currentNode);
continue;
}
if (regExpTest(/\/>/i, value)) {
Expand All @@ -15487,19 +15491,16 @@
}
var lcTag = transformCaseFunc(currentNode.nodeName);
if (!_isValidAttribute(lcTag, lcName, value)) {
_removeAttribute(name, currentNode);
continue;
}
if (value !== initValue) {
try {
if (namespaceURI) {
currentNode.setAttributeNS(namespaceURI, name, value);
} else {
currentNode.setAttribute(name, value);
}
} catch (_) {
_removeAttribute(name, currentNode);
try {
if (namespaceURI) {
currentNode.setAttributeNS(namespaceURI, name, value);
} else {
currentNode.setAttribute(name, value);
}
arrayPop(DOMPurify.removed);
} catch (_) {
}
}
_executeHook('afterSanitizeAttributes', currentNode, null);
Expand Down Expand Up @@ -16596,6 +16597,7 @@
const defaultedSettings = {
validate: true,
root_name: 'body',
sanitize: true,
...settings
};
const parser = new DOMParser();
Expand All @@ -16606,8 +16608,10 @@
const content = isSpecialRoot ? `<${ rootName }>${ html }</${ rootName }>` : html;
const wrappedHtml = format === 'xhtml' ? `<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>${ content }</body></html>` : `<body>${ content }</body>`;
const body = parser.parseFromString(wrappedHtml, mimeType).body;
purify.sanitize(body, getPurifyConfig(defaultedSettings, mimeType));
purify.removed = [];
if (defaultedSettings.sanitize) {
purify.sanitize(body, getPurifyConfig(defaultedSettings, mimeType));
purify.removed = [];
}
return isSpecialRoot ? body.firstChild : body;
};
const addNodeFilter = nodeFilterRegistry.addFilter;
Expand Down Expand Up @@ -16720,7 +16724,7 @@
};

const serializeContent = content => isTreeNode(content) ? HtmlSerializer({ validate: false }).serialize(content) : content;
const withSerializedContent = (content, fireEvent) => {
const withSerializedContent = (content, fireEvent, sanitize) => {
const serializedContent = serializeContent(content);
const eventArgs = fireEvent(serializedContent);
if (eventArgs.isDefaultPrevented()) {
Expand All @@ -16729,7 +16733,8 @@
if (eventArgs.content !== serializedContent) {
const rootNode = DomParser({
validate: false,
forced_root_block: false
forced_root_block: false,
sanitize
}).parse(eventArgs.content, { context: content.name });
return {
...eventArgs,
Expand Down Expand Up @@ -16764,10 +16769,10 @@
if (args.no_events) {
return content;
} else {
const processedEventArgs = withSerializedContent(content, c => fireGetContent(editor, {
const processedEventArgs = withSerializedContent(content, content => fireGetContent(editor, {
...args,
content: c
}));
content
}), shouldSanitizeXss(editor));
return processedEventArgs.content;
}
};
Expand All @@ -16778,7 +16783,7 @@
const processedEventArgs = withSerializedContent(args.content, content => fireBeforeSetContent(editor, {
...args,
content
}));
}), shouldSanitizeXss(editor));
if (processedEventArgs.isDefaultPrevented()) {
fireSetContent(editor, processedEventArgs);
return Result.error(undefined);
Expand Down Expand Up @@ -24409,7 +24414,7 @@
};

const preProcess = (editor, html) => {
const parser = DomParser({}, editor.schema);
const parser = DomParser({ sanitize: shouldSanitizeXss(editor) }, editor.schema);
parser.addNodeFilter('meta', nodes => {
Tools.each(nodes, node => {
node.remove();
Expand Down Expand Up @@ -26655,6 +26660,7 @@
remove_trailing_brs: getOption('remove_trailing_brs'),
inline_styles: getOption('inline_styles'),
root_name: getRootName(editor),
sanitize: getOption('xss_sanitization'),
validate: true,
blob_cache: blobCache,
document: editor.getDoc()
Expand Down
2 changes: 1 addition & 1 deletion lib/editor/tiny/js/tinymce/tinymce.min.js

Large diffs are not rendered by default.

0 comments on commit e8eb894

Please sign in to comment.