diff --git a/modules/backend/behaviors/FormController.php b/modules/backend/behaviors/FormController.php index b918a89c3..e7858b63a 100644 --- a/modules/backend/behaviors/FormController.php +++ b/modules/backend/behaviors/FormController.php @@ -258,7 +258,7 @@ class FormController extends ControllerBehavior Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.create_success')); - if ($redirect = $this->makeRedirect('create', $model)) { + if ($redirect = $this->makeRedirect($this->context, $model)) { return $redirect; } } @@ -326,7 +326,7 @@ class FormController extends ControllerBehavior Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.update_success')); - if ($redirect = $this->makeRedirect('update', $model)) { + if ($redirect = $this->makeRedirect($this->context, $model)) { return $redirect; } } diff --git a/modules/backend/classes/AuthManager.php b/modules/backend/classes/AuthManager.php index 389931686..85a59ae2f 100644 --- a/modules/backend/classes/AuthManager.php +++ b/modules/backend/classes/AuthManager.php @@ -60,6 +60,7 @@ class AuthManager extends RainAuthManager protected function init() { $this->useThrottle = Config::get('auth.throttle.enabled', true); + parent::init(); } /** diff --git a/modules/backend/classes/NavigationManager.php b/modules/backend/classes/NavigationManager.php index 4fb31c9aa..ee57c1666 100644 --- a/modules/backend/classes/NavigationManager.php +++ b/modules/backend/classes/NavigationManager.php @@ -373,7 +373,7 @@ class NavigationManager } } - if (empty($item->counter)) { + if (empty($item->counter) || !is_numeric($item->counter)) { $item->counter = null; } } @@ -421,6 +421,9 @@ class NavigationManager $item->counter = null; } } + if (!is_null($item->counter) && !is_numeric($item->counter)) { + throw new SystemException("The menu item {$activeItem->code}.{$item->code}'s counter property is invalid. Check to make sure it's numeric or callable. Value: " . var_export($item->counter, true)); + } } return $items; diff --git a/modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js b/modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js index eb6cc9bbf..6ed188929 100644 --- a/modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js +++ b/modules/backend/formwidgets/mediafinder/assets/js/mediafinder.js @@ -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 diff --git a/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js b/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js index c9c6aa38e..95c153829 100644 --- a/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js +++ b/modules/backend/formwidgets/richeditor/assets/js/build-plugins-min.js @@ -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]) diff --git a/modules/backend/formwidgets/richeditor/assets/js/richeditor.js b/modules/backend/formwidgets/richeditor/assets/js/richeditor.js index a7f3b91e4..0fcb1227c 100755 --- a/modules/backend/formwidgets/richeditor/assets/js/richeditor.js +++ b/modules/backend/formwidgets/richeditor/assets/js/richeditor.js @@ -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) { diff --git a/modules/system/assets/js/framework-min.js b/modules/system/assets/js/framework-min.js index d5ff8234b..a75e43143 100644 --- a/modules/system/assets/js/framework-min.js +++ b/modules/system/assets/js/framework-min.js @@ -185,4 +185,6 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i'+html+'',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 diff --git a/modules/system/assets/js/framework.combined-min.js b/modules/system/assets/js/framework.combined-min.js index 52c68980d..382a869ec 100644 --- a/modules/system/assets/js/framework.combined-min.js +++ b/modules/system/assets/js/framework.combined-min.js @@ -185,7 +185,9 @@ if(str[0]==="["){var result="[";var type="needBody";for(var i=1;i'+html+'',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})}}) diff --git a/modules/system/assets/js/framework.js b/modules/system/assets/js/framework.js index 6a6723ec7..c97a1554c 100644 --- a/modules/system/assets/js/framework.js +++ b/modules/system/assets/js/framework.js @@ -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('
'); + * $.parseHTML('
', null); + * + * with context document execute script! + * $.parseHTML('
', 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 attribute. + * + * will return [] + * $.parseHTML('