Skip to content

Commit

Permalink
Release 19.2.15
Browse files Browse the repository at this point in the history
Cherry-picked changesets:
  5c0dcb9 Anton Kuznetsov - HtmlEditor: prevent XSS vulnerability (script tag and inline handlers) (#22907) (#22920)
  • Loading branch information
alexslavr committed Oct 21, 2022
1 parent ae3dffd commit 2ca7e68
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 13 deletions.
3 changes: 0 additions & 3 deletions .vscode/extensions.json

This file was deleted.

5 changes: 0 additions & 5 deletions .vscode/settings.json

This file was deleted.

61 changes: 56 additions & 5 deletions js/ui/html_editor/ui.html_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,54 @@ const HtmlEditor = Editor.inherit({
return this._$submitElement;
},

_removeXSSVulnerableHtml: function(value) {
// NOTE: Script tags and inline handlers are removed to prevent XSS attacks.
// "Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set."
// error can be logged to the console if the html value is XSS vulnerable.

const $frame = $('<iframe>')
.css('display', 'none')
.attr({
id: 'xss-frame',
sandbox: 'allow-same-origin'
})
.appendTo('body');

const frame = $frame.get(0);
const frameWindow = frame.contentWindow;
const frameDocument = frameWindow.document;
const frameDocumentBody = frameDocument.body;

frameDocumentBody.innerHTML = value;

const removeInlineHandlers = (element) => {
if(element.attributes) {
for(let i = 0; i < element.attributes.length; i++) {
const name = element.attributes[i].name;
if(name.indexOf('on') === 0) {
element.removeAttribute(name);
}
}
}
if(element.childNodes) {
for(let i = 0; i < element.childNodes.length; i++) {
removeInlineHandlers(element.childNodes[i]);
}
}
};

removeInlineHandlers(frameDocumentBody);

$(frameDocumentBody)
.find('script')
.remove();

const sanitizedHtml = frameDocumentBody.innerHTML;

$frame.remove();
return sanitizedHtml;
},

_updateContainerMarkup: function() {
let markup = this.option('value');

Expand All @@ -175,7 +223,8 @@ const HtmlEditor = Editor.inherit({
}

if(markup) {
this._$htmlContainer.html(markup);
const sanitizedMarkup = this._removeXSSVulnerableHtml(markup);
this._$htmlContainer.html(sanitizedMarkup);
}
},

Expand Down Expand Up @@ -426,22 +475,24 @@ const HtmlEditor = Editor.inherit({

_optionChanged: function(args) {
switch(args.name) {
case 'value':
case 'value': {
const sanitizedValue = args.value ? this._removeXSSVulnerableHtml(args.value) : args.value;
if(this._quillInstance) {
if(this._isEditorUpdating) {
this._isEditorUpdating = false;
} else {
const updatedValue = this._isMarkdownValue() ? this._updateValueByType('HTML', args.value) : args.value;
const updatedValue = this._isMarkdownValue() ? this._updateValueByType('HTML', sanitizedValue) : sanitizedValue;
this._updateHtmlContent(updatedValue);
}
} else {
this._$htmlContainer.html(args.value);
this._$htmlContainer.html(sanitizedValue);
}

this._setSubmitValue(args.value);
this._setSubmitValue(sanitizedValue);

this.callBase(args);
break;
}
case 'placeholder':
case 'variables':
case 'toolbar':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,69 @@ QUnit.module('Value as HTML markup', moduleConfig, () => {
});
});

QUnit.module('xss security', {
beforeEach: function() {
window._isScriptExecuted = false;
window._isInlineHandlerExecuted = false;

this.htmlWithScript = '<script>window._isScriptExecuted = true;</script>';
this.htmlWithInlineHandler = '<img src="undefined" onerror="window._isInlineHandlerExecuted = true;"/>';
},
afterEach: function() {
delete window._isScriptExecuted;
delete window._isInlineHandlerExecuted;
}
}, () => {
test('script embedded in html value should not be executed on init', function(assert) {
const done = assert.async();

$('#htmlEditor').dxHtmlEditor({
value: this.htmlWithScript
});

setTimeout(() => {
assert.strictEqual(window._isScriptExecuted, false, 'script was not executed');
done();
}, 100);
});

test('inline handler embedded in html value should not be executed on init', function(assert) {
const done = assert.async();

$('#htmlEditor').dxHtmlEditor({
value: this.htmlWithInlineHandler
});

setTimeout(() => {
assert.strictEqual(window._isInlineHandlerExecuted, false, 'inline handler was not executed');
done();
}, 100);
});

test('value change to html with embedded script should not execute the script', function(assert) {
const done = assert.async();

const htmlEditor = $('#htmlEditor').dxHtmlEditor({}).dxHtmlEditor('instance');
htmlEditor.option('value', this.htmlWithScript);

setTimeout(() => {
assert.strictEqual(window._isScriptExecuted, false, 'script was not executed');
done();
}, 100);
});

test('value change to html with embedded inline handler should not execute the handler', function(assert) {
const done = assert.async();

const htmlEditor = $('#htmlEditor').dxHtmlEditor({}).dxHtmlEditor('instance');
htmlEditor.option('value', this.htmlWithInlineHandler);

setTimeout(() => {
assert.strictEqual(window._isInlineHandlerExecuted, false, 'inline handler was not executed');
done();
}, 100);
});
});

QUnit.module('Value as Markdown markup', {
beforeEach: function() {
Expand Down

0 comments on commit 2ca7e68

Please sign in to comment.