]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Add sanitize for tooltips and popovers html content.
authorJohann-S <johann.servoire@gmail.com>
Tue, 12 Feb 2019 15:24:35 +0000 (17:24 +0200)
committerXhmikosR <xhmikosr@gmail.com>
Wed, 13 Feb 2019 15:55:38 +0000 (17:55 +0200)
On browsers that `createHTMLDocument` isn't available just return the unsafe HTML.

js/popover.js
js/tests/unit/popover.js
js/tests/unit/tooltip.js
js/tooltip.js

index f00625b5d180b46a8cfde12c26219e7332e4e3f4..e64c3517940a574837cb26631c47ebf1efba2bd1 100644 (file)
     var title   = this.getTitle()
     var content = this.getContent()
 
-    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
-    $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
-      this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
-    ](content)
+    if (this.options.html) {
+      var typeContent = typeof content
+
+      if (this.options.sanitize) {
+        title = this.sanitizeHtml(title)
+
+        if (typeContent === 'string') {
+          content = this.sanitizeHtml(content)
+        }
+      }
+
+      $tip.find('.popover-title').html(title)
+      $tip.find('.popover-content').children().detach().end()[
+        typeContent === 'string' ? 'html' : 'append'
+      ](content)
+    } else {
+      $tip.find('.popover-title').text(title)
+      $tip.find('.popover-content').children().detach().end().text(content)
+    }
 
     $tip.removeClass('fade top bottom left right in')
 
index dc55ba49b5b8cd7e83ccb86668a15f81cc287a9b..f8fd613335424bf67f410265ce4ebc3f732f8f17 100644 (file)
@@ -190,7 +190,7 @@ $(function () {
       .bootstrapPopover({
         title: 'Test',
         content: 'Test',
-        template: '<div class="popover foobar"><div class="arrow"></div><div class="inner"><h3 class="title"/><div class="content"><p/></div></div></div>'
+        template: '<div class="popover foobar"><div class="arrow"></div><div class="inner"><h3 class="title"></h3><div class="content"><p></p></div></div></div>'
       })
       .one('shown.bs.popover', function () {
         assert.notEqual($('.popover').length, 0, 'popover was inserted')
index af319ba6ee8e31ea14cada04a0e724af5536451c..d26224f56c27e522180d45b2df0f895f21d3520f 100644 (file)
@@ -1526,4 +1526,184 @@ $(function () {
       }
     })
   })
+
+  QUnit.test('should disable sanitizer', function (assert) {
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        sanitize: false
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+    assert.strictEqual(tooltip.options.sanitize, false)
+  })
+
+  QUnit.test('should sanitize template by removing disallowed tags', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<div>',
+          '  <script>console.log("oups script inserted")</script>',
+          '  <span>Some content</span>',
+          '</div>'
+        ].join('')
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+    assert.strictEqual(tooltip.options.template.indexOf('script'), -1)
+  })
+
+  QUnit.test('should sanitize template by removing disallowed attributes', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<div>',
+          '  <img src="x" onError="alert(\'test\')">Some content</img>',
+          '</div>'
+        ].join('')
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+    assert.strictEqual(tooltip.options.template.indexOf('onError'), -1)
+  })
+
+  QUnit.test('should sanitize template by removing tags with XSS', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<div>',
+          '  <a href="javascript:alert(7)">Click me</a>',
+          '  <span>Some content</span>',
+          '</div>'
+        ].join('')
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+    assert.strictEqual(tooltip.options.template.indexOf('javascript'), -1)
+  })
+
+  QUnit.test('should allow custom sanitization rules', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(2)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<a href="javascript:alert(7)">Click me</a>',
+          '<span>Some content</span>'
+        ].join(''),
+        whiteList: {
+          span: null
+        }
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+
+    assert.strictEqual(tooltip.options.template.indexOf('<a'), -1)
+    assert.ok(tooltip.options.template.indexOf('span') !== -1)
+  })
+
+  QUnit.test('should allow passing a custom function for sanitization', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<span>Some content</span>'
+        ].join(''),
+        sanitizeFn: function (input) {
+          return input
+        }
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+
+    assert.ok(tooltip.options.template.indexOf('span') !== -1)
+  })
+
+  QUnit.test('should allow passing aria attributes', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<span aria-pressed="true">Some content</span>'
+        ].join('')
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+
+    assert.ok(tooltip.options.template.indexOf('aria-pressed') !== -1)
+  })
+
+  QUnit.test('should not take into account sanitize in data attributes', function (assert) {
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      assert.expect(0)
+
+      return
+    }
+
+    assert.expect(1)
+
+    var $trigger = $('<a href="#" rel="tooltip" data-sanitize="false" data-trigger="click" title="Another tooltip"/>')
+      .appendTo('#qunit-fixture')
+      .bootstrapTooltip({
+        template: [
+          '<span aria-pressed="true">Some content</span>'
+        ].join('')
+      })
+
+    var tooltip = $trigger.data('bs.tooltip')
+
+    assert.strictEqual(tooltip.options.sanitize, true)
+  })
 })
index 9663148559839923a8ca6024a7b979eaabc798bc..bd6252ff3b6326e24b41272e597014d28a64cfdb 100644 (file)
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  * ======================================================================== */
 
-
 +function ($) {
   'use strict';
 
+  var DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']
+
+  var uriAttrs = [
+    'background',
+    'cite',
+    'href',
+    'itemtype',
+    'longdesc',
+    'poster',
+    'src',
+    'xlink:href'
+  ]
+
+  var ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
+
+  var DefaultWhitelist = {
+    // Global attributes allowed on any supplied element below.
+    '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],
+    a: ['target', 'href', 'title', 'rel'],
+    area: [],
+    b: [],
+    br: [],
+    col: [],
+    code: [],
+    div: [],
+    em: [],
+    hr: [],
+    h1: [],
+    h2: [],
+    h3: [],
+    h4: [],
+    h5: [],
+    h6: [],
+    i: [],
+    img: ['src', 'alt', 'title', 'width', 'height'],
+    li: [],
+    ol: [],
+    p: [],
+    pre: [],
+    s: [],
+    small: [],
+    span: [],
+    sub: [],
+    sup: [],
+    strong: [],
+    u: [],
+    ul: []
+  }
+
+  /**
+   * A pattern that recognizes a commonly useful subset of URLs that are safe.
+   *
+   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+   */
+  var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi
+
+  /**
+   * A pattern that matches safe data URLs. Only matches image, video and audio types.
+   *
+   * Shoutout to Angular 7 https://github.com/angular/angular/blob/7.2.4/packages/core/src/sanitization/url_sanitizer.ts
+   */
+  var DATA_URL_PATTERN = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
+
+  function allowedAttribute(attr, allowedAttributeList) {
+    const attrName = attr.nodeName.toLowerCase()
+
+    if ($.inArray(attrName, allowedAttributeList) !== -1) {
+      if ($.inArray(attrName, uriAttrs) !== -1) {
+        return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
+      }
+
+      return true
+    }
+
+    var regExp = $(allowedAttributeList).filter(function (index, value) {
+      return value instanceof RegExp
+    })
+
+    // Check if a regular expression validates the attribute.
+    for (var i = 0, l = regExp.length; i < l; i++) {
+      if (attrName.match(regExp[i])) {
+        return true
+      }
+    }
+
+    return false
+  }
+
+  function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
+    if (unsafeHtml.length === 0) {
+      return unsafeHtml
+    }
+
+    if (sanitizeFn && typeof sanitizeFn === 'function') {
+      return sanitizeFn(unsafeHtml)
+    }
+
+    // IE 8 and below don't support createHTMLDocument
+    if (!document.implementation || !document.implementation.createHTMLDocument) {
+      return unsafeHtml
+    }
+
+    var createdDocument = document.implementation.createHTMLDocument('sanitization')
+    createdDocument.body.innerHTML = unsafeHtml
+
+    var whitelistKeys = Object.keys(whiteList)
+    var elements = $(createdDocument.body).find('*')
+
+    for (var i = 0, len = elements.length; i < len; i++) {
+      var el = elements[i]
+      var elName = el.nodeName.toLowerCase()
+
+      if ($.inArray(elName, whitelistKeys) === -1) {
+        el.parentNode.removeChild(el)
+
+        continue
+      }
+
+      var attributeList = $.map(el.attributes, function (el) { return el })
+      var whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])
+
+      attributeList.forEach((attr) => {
+        if (!allowedAttribute(attr, whitelistedAttributes)) {
+          el.removeAttribute(attr.nodeName)
+        }
+      })
+    }
+
+    return createdDocument.body.innerHTML
+  }
+
   // TOOLTIP PUBLIC CLASS DEFINITION
   // ===============================
 
     viewport: {
       selector: 'body',
       padding: 0
-    }
+    },
+    sanitize : true,
+    sanitizeFn : null,
+    whiteList : DefaultWhitelist
   }
 
   Tooltip.prototype.init = function (type, element, options) {
   }
 
   Tooltip.prototype.getOptions = function (options) {
-    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+    const dataAttributes = this.$element.data()
+
+    for (var dataAttr in dataAttributes) {
+      if (dataAttributes.hasOwnProperty(dataAttr) && $.inArray(dataAttr, DISALLOWED_ATTRIBUTES) !== -1) {
+        delete dataAttributes[dataAttr]
+      }
+    }
+
+    options = $.extend({}, this.getDefaults(), dataAttributes, options)
 
     if (options.delay && typeof options.delay == 'number') {
       options.delay = {
       }
     }
 
+    if (options.sanitize) {
+      config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
+    }
+
     return options
   }
 
     var $tip  = this.tip()
     var title = this.getTitle()
 
-    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    if (this.options.html) {
+      if (this.options.sanitize) {
+        title = sanitizeHtml(title, this.options.whiteList, this.options.sanitizeFn)
+      }
+
+      $tip.find('.tooltip-inner').html(title)
+    } else {
+      $tip.find('.tooltip-inner').text(title)
+    }
+
     $tip.removeClass('fade in top bottom left right')
   }
 
     })
   }
 
+  Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
+    return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
+  }
 
   // TOOLTIP PLUGIN DEFINITION
   // =========================