Skip to content

Commit b384954

Browse files
author
Luke Towers
committed
Improve Froala sanitization of pasted content.
1 parent 0cdda52 commit b384954

File tree

6 files changed

+85
-4
lines changed

6 files changed

+85
-4
lines changed

modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* - data-option="value" - an option with a value
77
*
88
* JavaScript API:
9-
* $('a#someElement').recordFinder({ option: 'value' })
9+
* $('a#someElement').mediaFinder({ option: 'value' })
1010
*
1111
* Dependences:
1212
* - Some other plugin (filename.js)
@@ -71,7 +71,7 @@
7171
this.$findValue = null
7272
this.$el = null
7373

74-
// In some cases options could contain callbacks,
74+
// In some cases options could contain callbacks,
7575
// so it's better to clean them up too.
7676
this.options = null
7777

modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ this.$textarea.on('froalaEditor.initialized',this.proxy(this.build))
194194
this.$textarea.on('froalaEditor.contentChanged',this.proxy(this.onChange))
195195
this.$textarea.on('froalaEditor.html.get',this.proxy(this.onSyncContent))
196196
this.$textarea.on('froalaEditor.html.set',this.proxy(this.onSetContent))
197+
this.$textarea.on('froalaEditor.paste.beforeCleanup',this.proxy(this.beforeCleanupPaste))
197198
this.$form.on('oc.beforeRequest',this.proxy(this.onFormBeforeRequest))
198199
this.$textarea.froalaEditor(froalaOptions)
199200
this.editor=this.$textarea.data('froala.editor')
@@ -213,6 +214,7 @@ this.$textarea.off('froalaEditor.initialized',this.proxy(this.build))
213214
this.$textarea.off('froalaEditor.contentChanged',this.proxy(this.onChange))
214215
this.$textarea.off('froalaEditor.html.get',this.proxy(this.onSyncContent))
215216
this.$textarea.off('froalaEditor.html.set',this.proxy(this.onSetContent))
217+
this.$textarea.off('froalaEditor.paste.beforeCleanup',this.proxy(this.beforeCleanupPaste))
216218
this.$form.off('oc.beforeRequest',this.proxy(this.onFormBeforeRequest))
217219
$(window).off('resize',this.proxy(this.updateLayout))
218220
$(window).off('oc.updateUi',this.proxy(this.updateLayout))
@@ -243,6 +245,7 @@ RichEditor.prototype.insertUiBlock=function($node){this.$textarea.froalaEditor('
243245
RichEditor.prototype.insertVideo=function(url,title){this.$textarea.froalaEditor('figures.insertVideo',url,title)}
244246
RichEditor.prototype.insertAudio=function(url,title){this.$textarea.froalaEditor('figures.insertAudio',url,title)}
245247
RichEditor.prototype.onSetContent=function(ev,editor){this.$textarea.trigger('setContent.oc.richeditor',[this])}
248+
RichEditor.prototype.beforeCleanupPaste=function(ev,editor,clipboard_html){return ocSanitize(clipboard_html)}
246249
RichEditor.prototype.onSyncContent=function(ev,editor,html){if(editor.codeBeautifier){html=editor.codeBeautifier.run(html,editor.opts.codeBeautifierOptions)}
247250
var container={html:html}
248251
this.$textarea.trigger('syncContent.oc.richeditor',[this,container])

modules/backend/formwidgets/richeditor/assets/js/richeditor.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@
209209
this.$textarea.on('froalaEditor.contentChanged', this.proxy(this.onChange))
210210
this.$textarea.on('froalaEditor.html.get', this.proxy(this.onSyncContent))
211211
this.$textarea.on('froalaEditor.html.set', this.proxy(this.onSetContent))
212+
this.$textarea.on('froalaEditor.paste.beforeCleanup', this.proxy(this.beforeCleanupPaste))
212213
this.$form.on('oc.beforeRequest', this.proxy(this.onFormBeforeRequest))
213214

214215
this.$textarea.froalaEditor(froalaOptions)
@@ -245,6 +246,7 @@
245246
this.$textarea.off('froalaEditor.contentChanged', this.proxy(this.onChange))
246247
this.$textarea.off('froalaEditor.html.get', this.proxy(this.onSyncContent))
247248
this.$textarea.off('froalaEditor.html.set', this.proxy(this.onSetContent))
249+
this.$textarea.off('froalaEditor.paste.beforeCleanup', this.proxy(this.beforeCleanupPaste))
248250
this.$form.off('oc.beforeRequest', this.proxy(this.onFormBeforeRequest))
249251

250252
$(window).off('resize', this.proxy(this.updateLayout))
@@ -344,6 +346,10 @@
344346
this.$textarea.trigger('setContent.oc.richeditor', [this])
345347
}
346348

349+
RichEditor.prototype.beforeCleanupPaste = function (ev, editor, clipboard_html) {
350+
return ocSanitize(clipboard_html)
351+
}
352+
347353
RichEditor.prototype.onSyncContent = function(ev, editor, html) {
348354
// Beautify HTML.
349355
if (editor.codeBeautifier) {

modules/system/assets/js/framework-min.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,6 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i<str.length;i++
185185
if(str[i]==="]"&&i===str.length-1){if(result[result.length-1]===",")result=result.substr(0,result.length-1);result+="]";return result;}
186186
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;}}}
187187
throw new Error("Broken JSON array near "+result);}}
188-
window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window);
188+
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);}});}
189+
function sanitize(html){var output=$($.parseHTML('<div>'+html+'</div>',null,false));output.find('*').each(function(){trimAttributes(this);});return output.html();}
190+
window.ocSanitize=function(html){return sanitize(html)};}(window);

modules/system/assets/js/framework.combined-min.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i<str.length;i++
185185
if(str[i]==="]"&&i===str.length-1){if(result[result.length-1]===",")result=result.substr(0,result.length-1);result+="]";return result;}
186186
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;}}}
187187
throw new Error("Broken JSON array near "+result);}}
188-
window.ocJSON=function(json){var jsonString=parse(json);return JSON.parse(jsonString);};}(window);+function($){"use strict";if($.oc===undefined)
188+
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);}});}
189+
function sanitize(html){var output=$($.parseHTML('<div>'+html+'</div>',null,false));output.find('*').each(function(){trimAttributes(this);});return output.html();}
190+
window.ocSanitize=function(html){return sanitize(html)};}(window);+function($){"use strict";if($.oc===undefined)
189191
$.oc={}
190192
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'})}
191193
context.options.handleFlashMessage=function(message,type){$.oc.flashMsg({text:message,class:type})}})

modules/system/assets/js/framework.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,3 +907,71 @@ if (window.jQuery.request !== undefined) {
907907
};
908908

909909
}(window);
910+
911+
/*
912+
* October CMS jQuery HTML Sanitizer
913+
* @see https://gist.github.com/ufologist/5a0da51b2b9ef1b861c30254172ac3c9
914+
*/
915+
+function(window) { "use strict";
916+
917+
function trimAttributes(node) {
918+
$.each(node.attributes, function() {
919+
var attrName = this.name;
920+
var attrValue = this.value;
921+
922+
/*
923+
* remove attributes where the names start with "on" (for example: onload, onerror...)
924+
* remove attributes where the value starts with the "javascript:" pseudo protocol (for example href="javascript:alert(1)")
925+
*/
926+
if (attrName.indexOf('on') == 0 || attrValue.indexOf('javascript:') == 0) {
927+
$(node).removeAttr(attrName);
928+
}
929+
});
930+
}
931+
932+
function sanitize(html) {
933+
/*
934+
* [jQuery.parseHTML(data [, context ] [, keepScripts ])](http://api.jquery.com/jQuery.parseHTML/) added: 1.8
935+
* Parses a string into an array of DOM nodes.
936+
*
937+
* By default, the context is the current document if not specified or given as null or undefined. If the HTML was to be used
938+
* in another document such as an iframe, that frame's document could be used.
939+
*
940+
* As of 3.0 the default behavior is changed.
941+
*
942+
* If the context is not specified or given as null or undefined, a new document is used.
943+
* This can potentially improve security because inline events will not execute when the HTML is parsed. Once the parsed HTML
944+
* is injected into a document it does execute, but this gives tools a chance to traverse the created DOM and remove anything
945+
* deemed unsafe. This improvement does not apply to internal uses of jQuery.parseHTML as they usually pass in the current
946+
* document. Therefore, a statement like $( "#log" ).append( $( htmlString ) ) is still subject to the injection of malicious code.
947+
*
948+
* without context do not execute script
949+
* $.parseHTML('<div><img src=1 onerror=alert(1)></div>');
950+
* $.parseHTML('<div><img src=1 onerror=alert(2)></div>', null);
951+
*
952+
* with context document execute script!
953+
* $.parseHTML('<div><img src=1 onerror=alert(3)></div>', document);
954+
*
955+
* Most jQuery APIs that accept HTML strings will run scripts that are included in the HTML. jQuery.parseHTML does not run scripts
956+
* in the parsed HTML unless keepScripts is explicitly true. However, it is still possible in most environments to execute scripts
957+
* indirectly, for example via the <img onerror> attribute.
958+
*
959+
* will return []
960+
* $.parseHTML('<script>alert(1)<\/script>', null, false);
961+
*
962+
* will return [script DOM element]
963+
* $.parseHTML('<script>alert(1)<\/script>', null, true);
964+
*/
965+
var output = $($.parseHTML('<div>' + html + '</div>', null, false));
966+
output.find('*').each(function() {
967+
trimAttributes(this);
968+
});
969+
return output.html();
970+
}
971+
972+
// Global function
973+
window.ocSanitize = function(html) {
974+
return sanitize(html)
975+
};
976+
977+
}(window);

0 commit comments

Comments
 (0)