Browse Source

Implement CSP compliance for HTML markup via templates fix #1565 fix #1571

Kartik Visweswaran 5 years ago
parent
commit
aa998828a5
3 changed files with 114 additions and 25 deletions
  1. 1 0
      CHANGE.md
  2. 113 25
      js/fileinput.js
  3. 0 0
      js/fileinput.min.js

+ 1 - 0
CHANGE.md

@@ -5,6 +5,7 @@ Change Log: `bootstrap-fileinput`
 
 **Date:** _under development_
 
+- (bug #1565, 1571): Implement CSP compliance for HTML markup via templates.
 - (bug #1569): Correctly validate persistent file validation errors and display them.
 - (enh #1568): Correct timeout and resumable validation for async uploads.
 - (enh #1536, #1567): Parse response as string in `_parseError` function.

+ 113 - 25
js/fileinput.js

@@ -447,8 +447,89 @@
         getElement: function (options, param, value) {
             return ($h.isEmpty(options) || $h.isEmpty(options[param])) ? value : $(options[param]);
         },
+        createElement: function (str, tag) {
+            tag = tag || 'div';
+            return $($.parseHTML('<' + tag + '>' + str + '</' + tag + '>'));
+        },
         uniqId: function () {
-            return Math.round(new Date().getTime()) + '_' + Math.round(Math.random() * 100);
+            return (new Date().getTime() + Math.floor(Math.random() * Math.pow(10, 15))).toString(36);
+        },
+        parseEventCallback: function (str) {
+            return Function("'use strict'; return (function() { " + str + " });")();
+        },
+        cspBuffer: {
+            CSP_ATTRIB: 'data-csp-01928735', // a randomly named temporary attribute to store the CSP elem id
+            domEventsList: [
+                'mousedown', 'mouseup', 'click', 'dblclick', 'mousemove', 'mouseover', 'mousewheel', 'mouseout',
+                'contextmenu', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'keydown', 'keypress', 'keyup',
+                'focus', 'blur', 'change', 'submit', 'scroll', 'resize', 'hashchange', 'load', 'unload',
+                'cut', 'copy', 'paste'
+            ],
+            domElementEvents: {},
+            domElementsStyles: {},
+            stash: function (htmlString) {
+                var self = this, outerDom = $.parseHTML('<div>' + htmlString + '</div>'), $el = $(outerDom);
+                $el.find('[style]').each(function (key, elem) {
+                    var $elem = $(elem), styleString = $elem.attr('style'), id = $h.uniqId(), styles = {};
+                    if (styleString && styleString.length) {
+                        if (styleString.indexOf(';') === -1) {
+                            styleString += ';'
+                        }
+                        styleString.slice(0, styleString.length - 1).split(';').map(function (str) {
+                            str = str.split(':');
+                            if (str[0]) {
+                                styles[str[0]] = str[1] ? str[1] : '';
+                            }
+                        });
+                        self.domElementsStyles[id] = styles;
+                        $elem.removeAttr('style').attr(self.CSP_ATTRIB, id);
+                    }
+                });
+                $el.filter('*').removeAttr('style');                   // make sure all style attr are removed
+                $.each(self.domEventsList, function (key, eventName) { // handle onXXX events set as inline markup
+                    var id, fn, event = 'on' + eventName, $inlineEvent = $el.find('[' + event + ']');
+                    if ($inlineEvent.length) {
+                        fn = $h.parseEventCallback($inlineEvent.attr(event));
+                        if ($inlineEvent.attr(self.CSP_ATTRIB)) {
+                            id = $inlineEvent.attr(self.CSP_ATTRIB);
+                        } else {
+                            id = $h.uniqId();
+                            self.domElementEvents[id] = [];
+                        }
+                        self.domElementEvents[id].push({name: eventName + '.csp', handler: fn}); // special csp namespace
+                        $inlineEvent.removeAttr(event).attr(self.CSP_ATTRIB, id);
+                    }
+                });
+                return Object.values(outerDom).flatMap(function (elem) {
+                    return elem.outerHTML;
+                }).join('');
+            },
+            apply: function (domElement) {
+                var self = this, $el = $(domElement);
+                $el.find('[' + self.CSP_ATTRIB + ']').each(function (key, elem) {
+                    var $elem = $(elem), id = $elem.attr(self.CSP_ATTRIB), styles = self.domElementsStyles[id],
+                        events = self.domElementEvents[id];
+                    if (styles) {
+                        $elem.css(styles);
+                    }
+                    if (events) {
+                        $.each(events, function (key, event) {
+                            if (event && event.name) {
+                                $elem.off(event.name).on(event.name, event.handler);
+                            }
+                        });
+                    }
+                    $elem.removeAttr(self.CSP_ATTRIB);
+                });
+                self.domElementsStyles = {};
+                self.domElementEvents = {};
+            }
+        },
+        setHtml: function ($elem, htmlString) {
+            var buf = $h.cspBuffer;
+            $elem.html(buf.stash(htmlString));
+            buf.apply($elem);
+            return $elem;
         },
         htmlEncode: function (str, undefVal) {
             if (str === undefined) {
@@ -1939,7 +2020,7 @@
                     }
                     /** @namespace config.frameAttr */
                     if (!$h.isEmpty(config) && !$h.isEmpty(config.frameAttr)) {
-                        $tmp = $(document.createElement('div')).html(out);
+                        $tmp = $h.createElement(out);
                         $tmp.find('.file-preview-initial').attr(config.frameAttr);
                         out = $tmp.html();
                         $tmp.remove();
@@ -2134,7 +2215,7 @@
             if ($errList.length) {
                 return true;
             }
-            $err = $(document.createElement('div')).html(self.$errorContainer.html());
+            $err = $h.createElement(self.$errorContainer.html());
             $err.find('.kv-error-close').remove();
             $err.find('ul').remove();
             return !!$.trim($err.text()).length;
@@ -2167,7 +2248,7 @@
         _addError: function (msg) {
             var self = this, $error = self.$errorContainer;
             if (msg && $error.length) {
-                $error.html(self.errorCloseButton + msg);
+                $h.setHtml($error, self.errorCloseButton + msg);
                 self._handler($error.find('.kv-error-close'), 'click', function () {
                     setTimeout(function () {
                         if (self.showPreview && !self.getFrames().length) {
@@ -2614,7 +2695,7 @@
         },
         _setPreviewContent: function (content) {
             var self = this;
-            self.$preview.html(content);
+            $h.setHtml(self.$preview, content);
             self._autoFitContent();
         },
         _initPreviewImageOrientations: function () {
@@ -2711,12 +2792,14 @@
             }
             self.$modal = $(modalId);
             if (!self.$modal || !self.$modal.length) {
-                $dialog = $(document.createElement('div')).html(modalMain).insertAfter(self.$container);
+                $dialog = $h.createElement($h.cspBuffer.stash(modalMain)).insertAfter(self.$container);
                 self.$modal = $(modalId).insertBefore($dialog);
+                $h.cspBuffer.apply(self.$modal);
                 $dialog.remove();
             }
             $h.initModal(self.$modal);
-            self.$modal.html(self._getModalContent());
+            self.$modal.html($h.cspBuffer.stash(self._getModalContent()));
+            $h.cspBuffer.apply(self.$modal);
             $.each($h.MODAL_EVENTS, function (key, event) {
                 self._listenModalEvent(event);
             });
@@ -2810,7 +2893,7 @@
             $modal.removeClass('kv-single-content');
             if (animate) {
                 $tmp = $body.addClass('file-thumb-loading').clone().insertAfter($body);
-                $body.html(body).hide();
+                $h.setHtml($body, body).hide();
                 $tmp.fadeOut('fast', function () {
                     $body.fadeIn('fast', function () {
                         $body.removeClass('file-thumb-loading');
@@ -2818,7 +2901,7 @@
                     $tmp.remove();
                 });
             } else {
-                $body.html(body);
+                $h.setHtml($body, body);
             }
             config = self.previewZoomSettings[tmplt];
             if (config) {
@@ -2887,7 +2970,7 @@
                 throw 'Cannot zoom to detailed preview!';
             }
             $h.initModal($modal);
-            $modal.html(self._getModalContent());
+            $h.setHtml($modal, self._getModalContent());
             $frame = $btn.closest($h.FRAMES);
             self._setZoomContent($frame);
             $modal.modal('show');
@@ -3248,7 +3331,7 @@
                     if (!allFiles) {
                         index = self.previewCache.add(content[0], config[0], tags[0], append);
                         data = self.previewCache.get(index, false);
-                        $div = $(document.createElement('div')).html(data).hide().insertAfter($thumb);
+                        $div = $h.createElement(data).hide().insertAfter($thumb);
                         $newCache = $div.find('.kv-zoom-cache');
                         if ($newCache && $newCache.length) {
                             $newCache.insertAfter($thumb);
@@ -3990,8 +4073,11 @@
             return prevContent + zoomContent;
         },
         _addToPreview: function ($preview, content) {
-            var self = this;
-            return self.reversePreviewOrder ? $preview.prepend(content) : $preview.append(content);
+            var self = this, $el;
+            content = $h.cspBuffer.stash(content);
+            $el = self.reversePreviewOrder ? $preview.prepend(content) : $preview.append(content);
+            $h.cspBuffer.apply($preview);
+            return $el;
         },
         _previewDefault: function (file, isDisabled) {
             var self = this, $preview = self.$preview;
@@ -4096,7 +4182,7 @@
             if (status === 'Success') {
                 $thumb.find('.file-drag-handle').remove();
             }
-            $indicator.html(config[icon]);
+            $h.setHtml($indicator, config[icon]);
             $indicator.attr('title', config[msg]);
             $thumb.addClass(css);
             if (status === 'Error' && !self.retryErrorUploads) {
@@ -4128,9 +4214,9 @@
                 }
                 stats = stats || '';
                 out = out.setTokens({stats: stats});
-                $el.html(out);
+                $h.setHtml($el, out);
                 if (error) {
-                    $el.find('[role="progressbar"]').html(error);
+                    $h.setHtml($el.find('[role="progressbar"]'), error);
                 }
             }
         },
@@ -4180,7 +4266,7 @@
         _toggleResumableProgress: function (template, message) {
             var self = this, $progress = self.$progress;
             if ($progress && $progress.length) {
-                $progress.html(template.setTokens({
+                $h.setHtml($progress, template.setTokens({
                     percent: 101,
                     status: message,
                     stats: ''
@@ -4274,7 +4360,8 @@
                 icon = cfg.uploadRetryIcon;
                 title = cfg.uploadRetryTitle;
             }
-            $btn.attr('title', title).html(icon);
+            $btn.attr('title', title);
+            $h.setHtml($btn, icon);
         },
         _checkDimensions: function (i, chk, $img, $thumb, fname, type, params) {
             var self = this, msg, dim, tag = chk === 'Small' ? 'min' : 'max', limit = self[tag + 'Image' + type],
@@ -4585,12 +4672,13 @@
             }
             self.$captionContainer.addClass('icon-visible');
             self.$caption.attr('title', title).val(out);
-            self.$captionIcon.html(icon);
+            $h.setHtml(self.$captionIcon, icon);
         },
         _createContainer: function () {
             var self = this, attribs = {'class': 'file-input file-input-new' + (self.rtl ? ' kv-rtl' : '')},
-                $container = $(document.createElement('div')).attr(attribs).html(self._renderMain());
-            $container.insertBefore(self.$element);
+                $container = $h.createElement($h.cspBuffer.stash(self._renderMain()));
+            $h.cspBuffer.apply($container);
+            $container.insertBefore(self.$element).attr(attribs);
             self._initBrowse($container);
             if (self.theme) {
                 $container.addClass('theme-' + self.theme);
@@ -4600,7 +4688,7 @@
         _refreshContainer: function () {
             var self = this, $container = self.$container, $el = self.$element;
             $el.insertAfter($container);
-            $container.html(self._renderMain());
+            $h.setHtml($container, self._renderMain());
             self._initBrowse($container);
             self._validateDisabled();
         },
@@ -5042,7 +5130,7 @@
                     if (self.duplicateErrors.length) {
                         errors = '<li>' + self.duplicateErrors.join('</li><li>') + '</li>';
                         if ($error.find('ul').length === 0) {
-                            $error.html(self.errorCloseButton + '<ul>' + errors + '</ul>');
+                            $h.setHtml($error, self.errorCloseButton + '<ul>' + errors + '</ul>');
                         } else {
                             $error.find('ul').append(errors);
                         }
@@ -5587,7 +5675,7 @@
                 return;
             }
             $h.initModal($modal);
-            $modal.html(self._getModalContent());
+            $h.setHtml($modal, self._getModalContent());
             self._setZoomContent($frame);
             $modal.modal('show');
             self._initZoomButtons();
@@ -5703,7 +5791,7 @@
         deleteExtraData: {},
         overwriteInitial: true,
         sanitizeZoomCache: function (content) {
-            var $container = $(document.createElement('div')).append(content);
+            var $container = $h.createElement(content);
             $container.find('input,select,.file-thumbnail-footer').remove();
             return $container.html();
         },

File diff suppressed because it is too large
+ 0 - 0
js/fileinput.min.js


Some files were not shown because too many files changed in this diff