Low severityNVD Advisory· Published Jul 2, 2020· Updated Aug 4, 2024
Cross-site Scripting in OctoberPotential self-XSS when pasting content from malicious websites
CVE-2020-4061
Description
In October from version 1.0.319 and before version 1.0.467, pasting content copied from malicious websites into the Froala richeditor could result in a successful self-XSS attack. This has been fixed in 1.0.467.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
october/backendPackagist | >= 1.0.319, < 1.0.467 | 1.0.467 |
Affected products
1- Range: >= 1.0.319, < 1.0.467
Patches
1b384954a29b8Improve Froala sanitization of pasted content.
6 files changed · +85 −4
modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js+2 −2 modified@@ -6,7 +6,7 @@ * - data-option="value" - an option with a value * * JavaScript API: - * $('a#someElement').recordFinder({ option: 'value' }) + * $('a#someElement').mediaFinder({ option: 'value' }) * * Dependences: * - Some other plugin (filename.js) @@ -71,7 +71,7 @@ this.$findValue = null this.$el = null - // In some cases options could contain callbacks, + // In some cases options could contain callbacks, // so it's better to clean them up too. this.options = null
modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js+3 −0 modified@@ -194,6 +194,7 @@ this.$textarea.on('froalaEditor.initialized',this.proxy(this.build)) this.$textarea.on('froalaEditor.contentChanged',this.proxy(this.onChange)) this.$textarea.on('froalaEditor.html.get',this.proxy(this.onSyncContent)) this.$textarea.on('froalaEditor.html.set',this.proxy(this.onSetContent)) +this.$textarea.on('froalaEditor.paste.beforeCleanup',this.proxy(this.beforeCleanupPaste)) this.$form.on('oc.beforeRequest',this.proxy(this.onFormBeforeRequest)) this.$textarea.froalaEditor(froalaOptions) this.editor=this.$textarea.data('froala.editor') @@ -213,6 +214,7 @@ this.$textarea.off('froalaEditor.initialized',this.proxy(this.build)) this.$textarea.off('froalaEditor.contentChanged',this.proxy(this.onChange)) this.$textarea.off('froalaEditor.html.get',this.proxy(this.onSyncContent)) this.$textarea.off('froalaEditor.html.set',this.proxy(this.onSetContent)) +this.$textarea.off('froalaEditor.paste.beforeCleanup',this.proxy(this.beforeCleanupPaste)) this.$form.off('oc.beforeRequest',this.proxy(this.onFormBeforeRequest)) $(window).off('resize',this.proxy(this.updateLayout)) $(window).off('oc.updateUi',this.proxy(this.updateLayout)) @@ -243,6 +245,7 @@ RichEditor.prototype.insertUiBlock=function($node){this.$textarea.froalaEditor(' RichEditor.prototype.insertVideo=function(url,title){this.$textarea.froalaEditor('figures.insertVideo',url,title)} RichEditor.prototype.insertAudio=function(url,title){this.$textarea.froalaEditor('figures.insertAudio',url,title)} RichEditor.prototype.onSetContent=function(ev,editor){this.$textarea.trigger('setContent.oc.richeditor',[this])} +RichEditor.prototype.beforeCleanupPaste=function(ev,editor,clipboard_html){return ocSanitize(clipboard_html)} RichEditor.prototype.onSyncContent=function(ev,editor,html){if(editor.codeBeautifier){html=editor.codeBeautifier.run(html,editor.opts.codeBeautifierOptions)} var container={html:html} this.$textarea.trigger('syncContent.oc.richeditor',[this,container])
modules/backend/formwidgets/richeditor/assets/js/richeditor.js+6 −0 modified@@ -209,6 +209,7 @@ this.$textarea.on('froalaEditor.contentChanged', this.proxy(this.onChange)) this.$textarea.on('froalaEditor.html.get', this.proxy(this.onSyncContent)) this.$textarea.on('froalaEditor.html.set', this.proxy(this.onSetContent)) + this.$textarea.on('froalaEditor.paste.beforeCleanup', this.proxy(this.beforeCleanupPaste)) this.$form.on('oc.beforeRequest', this.proxy(this.onFormBeforeRequest)) this.$textarea.froalaEditor(froalaOptions) @@ -245,6 +246,7 @@ this.$textarea.off('froalaEditor.contentChanged', this.proxy(this.onChange)) this.$textarea.off('froalaEditor.html.get', this.proxy(this.onSyncContent)) this.$textarea.off('froalaEditor.html.set', this.proxy(this.onSetContent)) + this.$textarea.off('froalaEditor.paste.beforeCleanup', this.proxy(this.beforeCleanupPaste)) this.$form.off('oc.beforeRequest', this.proxy(this.onFormBeforeRequest)) $(window).off('resize', this.proxy(this.updateLayout)) @@ -344,6 +346,10 @@ this.$textarea.trigger('setContent.oc.richeditor', [this]) } + RichEditor.prototype.beforeCleanupPaste = function (ev, editor, clipboard_html) { + return ocSanitize(clipboard_html) + } + RichEditor.prototype.onSyncContent = function(ev, editor, html) { // Beautify HTML. if (editor.codeBeautifier) {
modules/system/assets/js/framework.combined-min.js+3 −1 modified@@ -185,7 +185,9 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i<str.length;i++ if(str[i]==="]"&&i===str.length-1){if(result[result.length-1]===",")result=result.substr(0,result.length-1);result+="]";return result;} var body=getBody(str,i);i=i+body.originLength-1;result+=parse(body.body);type="afterBody";}else if(type==="afterBody"){if(str[i]===","){result+=",";type="needBody";while(str[i+1]===","||isBlankChar(str[i+1])){if(str[i+1]===",")result+="null,";i++;}}else if(str[i]==="]"&&i===str.length-1){result+="]";return result;}}} throw new Error("Broken JSON array near "+result);}} -window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window);+function($){"use strict";if($.oc===undefined) +window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window);+function(window){"use strict";function trimAttributes(node){$.each(node.attributes,function(){var attrName=this.name;var attrValue=this.value;if(attrName.indexOf('on')==0||attrValue.indexOf('javascript:')==0){$(node).removeAttr(attrName);}});} +function sanitize(html){var output=$($.parseHTML('<div>'+html+'</div>',null,false));output.find('*').each(function(){trimAttributes(this);});return output.html();} +window.ocSanitize=function(html){return sanitize(html)};}(window);+function($){"use strict";if($.oc===undefined) $.oc={} var LOADER_CLASS='oc-loading';$(document).on('ajaxSetup','[data-request][data-request-flash]',function(event,context){context.options.handleErrorMessage=function(message){$.oc.flashMsg({text:message,class:'error'})} context.options.handleFlashMessage=function(message,type){$.oc.flashMsg({text:message,class:type})}})
modules/system/assets/js/framework.js+68 −0 modified@@ -907,3 +907,71 @@ if (window.jQuery.request !== undefined) { }; }(window); + +/* + * October CMS jQuery HTML Sanitizer + * @see https://gist.github.com/ufologist/5a0da51b2b9ef1b861c30254172ac3c9 + */ ++function(window) { "use strict"; + + function trimAttributes(node) { + $.each(node.attributes, function() { + var attrName = this.name; + var attrValue = this.value; + + /* + * remove attributes where the names start with "on" (for example: onload, onerror...) + * remove attributes where the value starts with the "javascript:" pseudo protocol (for example href="javascript:alert(1)") + */ + if (attrName.indexOf('on') == 0 || attrValue.indexOf('javascript:') == 0) { + $(node).removeAttr(attrName); + } + }); + } + + function sanitize(html) { + /* + * [jQuery.parseHTML(data [, context ] [, keepScripts ])](http://api.jquery.com/jQuery.parseHTML/) added: 1.8 + * Parses a string into an array of DOM nodes. + * + * By default, the context is the current document if not specified or given as null or undefined. If the HTML was to be used + * in another document such as an iframe, that frame's document could be used. + * + * As of 3.0 the default behavior is changed. + * + * If the context is not specified or given as null or undefined, a new document is used. + * This can potentially improve security because inline events will not execute when the HTML is parsed. Once the parsed HTML + * is injected into a document it does execute, but this gives tools a chance to traverse the created DOM and remove anything + * deemed unsafe. This improvement does not apply to internal uses of jQuery.parseHTML as they usually pass in the current + * document. Therefore, a statement like $( "#log" ).append( $( htmlString ) ) is still subject to the injection of malicious code. + * + * without context do not execute script + * $.parseHTML('<div><img src=1 onerror=alert(1)></div>'); + * $.parseHTML('<div><img src=1 onerror=alert(2)></div>', null); + * + * with context document execute script! + * $.parseHTML('<div><img src=1 onerror=alert(3)></div>', document); + * + * Most jQuery APIs that accept HTML strings will run scripts that are included in the HTML. jQuery.parseHTML does not run scripts + * in the parsed HTML unless keepScripts is explicitly true. However, it is still possible in most environments to execute scripts + * indirectly, for example via the <img onerror> attribute. + * + * will return [] + * $.parseHTML('<script>alert(1)<\/script>', null, false); + * + * will return [script DOM element] + * $.parseHTML('<script>alert(1)<\/script>', null, true); + */ + var output = $($.parseHTML('<div>' + html + '</div>', null, false)); + output.find('*').each(function() { + trimAttributes(this); + }); + return output.html(); + } + + // Global function + window.ocSanitize = function(html) { + return sanitize(html) + }; + +}(window); \ No newline at end of file
modules/system/assets/js/framework-min.js+3 −1 modified@@ -185,4 +185,6 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i<str.length;i++ if(str[i]==="]"&&i===str.length-1){if(result[result.length-1]===",")result=result.substr(0,result.length-1);result+="]";return result;} var body=getBody(str,i);i=i+body.originLength-1;result+=parse(body.body);type="afterBody";}else if(type==="afterBody"){if(str[i]===","){result+=",";type="needBody";while(str[i+1]===","||isBlankChar(str[i+1])){if(str[i+1]===",")result+="null,";i++;}}else if(str[i]==="]"&&i===str.length-1){result+="]";return result;}}} throw new Error("Broken JSON array near "+result);}} -window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window); \ No newline at end of file +window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window);+function(window){"use strict";function trimAttributes(node){$.each(node.attributes,function(){var attrName=this.name;var attrValue=this.value;if(attrName.indexOf('on')==0||attrValue.indexOf('javascript:')==0){$(node).removeAttr(attrName);}});} +function sanitize(html){var output=$($.parseHTML('<div>'+html+'</div>',null,false));output.find('*').each(function(){trimAttributes(this);});return output.html();} +window.ocSanitize=function(html){return sanitize(html)};}(window); \ No newline at end of file
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-3pc2-fm7p-q2vgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-4061ghsaADVISORY
- github.com/octobercms/october/commit/b384954a29b89117e1c0d6035b3ede4f46df67c5ghsax_refsource_MISCWEB
- github.com/octobercms/october/security/advisories/GHSA-3pc2-fm7p-q2vgghsax_refsource_CONFIRMWEB
- research.securitum.com/the-curious-case-of-copy-pasteghsaWEB
- research.securitum.com/the-curious-case-of-copy-paste/mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.