2
0
mirror of https://github.com/tenrok/bootstrap.git synced 2026-05-15 11:59:39 +03:00

Add sanitize for tooltips and popovers html content.

On browsers that `createHTMLDocument` isn't available just return the unsafe HTML.
This commit is contained in:
Johann-S
2019-02-12 17:24:35 +02:00
committed by XhmikosR
parent d4129dff60
commit 2c8abb9a43
4 changed files with 361 additions and 9 deletions
+19 -4
View File
@@ -45,10 +45,25 @@
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')
+1 -1
View 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')
+180
View 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)
})
})
+161 -4
View File
@@ -7,10 +7,140 @@
* 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
// ===============================
@@ -43,7 +173,10 @@
viewport: {
selector: 'body',
padding: 0
}
},
sanitize : true,
sanitizeFn : null,
whiteList : DefaultWhitelist
}
Tooltip.prototype.init = function (type, element, options) {
@@ -84,7 +217,15 @@
}
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 = {
@@ -93,6 +234,10 @@
}
}
if (options.sanitize) {
config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
}
return options
}
@@ -306,7 +451,16 @@
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')
}
@@ -487,6 +641,9 @@
})
}
Tooltip.prototype.sanitizeHtml = function (unsafeHtml) {
return sanitizeHtml(unsafeHtml, this.options.whiteList, this.options.sanitizeFn)
}
// TOOLTIP PLUGIN DEFINITION
// =========================