mirror of
https://github.com/tenrok/bootstrap.git
synced 2026-06-11 18:02:28 +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:
+19
-4
@@ -45,10 +45,25 @@
|
|||||||
var title = this.getTitle()
|
var title = this.getTitle()
|
||||||
var content = this.getContent()
|
var content = this.getContent()
|
||||||
|
|
||||||
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
|
if (this.options.html) {
|
||||||
$tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events
|
var typeContent = typeof content
|
||||||
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
|
|
||||||
](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')
|
$tip.removeClass('fade top bottom left right in')
|
||||||
|
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ $(function () {
|
|||||||
.bootstrapPopover({
|
.bootstrapPopover({
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
content: '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 () {
|
.one('shown.bs.popover', function () {
|
||||||
assert.notEqual($('.popover').length, 0, 'popover was inserted')
|
assert.notEqual($('.popover').length, 0, 'popover was inserted')
|
||||||
|
|||||||
@@ -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
@@ -7,10 +7,140 @@
|
|||||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
* ======================================================================== */
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
+function ($) {
|
+function ($) {
|
||||||
'use strict';
|
'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
|
// TOOLTIP PUBLIC CLASS DEFINITION
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
||||||
@@ -43,7 +173,10 @@
|
|||||||
viewport: {
|
viewport: {
|
||||||
selector: 'body',
|
selector: 'body',
|
||||||
padding: 0
|
padding: 0
|
||||||
}
|
},
|
||||||
|
sanitize : true,
|
||||||
|
sanitizeFn : null,
|
||||||
|
whiteList : DefaultWhitelist
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip.prototype.init = function (type, element, options) {
|
Tooltip.prototype.init = function (type, element, options) {
|
||||||
@@ -84,7 +217,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Tooltip.prototype.getOptions = function (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') {
|
if (options.delay && typeof options.delay == 'number') {
|
||||||
options.delay = {
|
options.delay = {
|
||||||
@@ -93,6 +234,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.sanitize) {
|
||||||
|
config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
|
||||||
|
}
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +451,16 @@
|
|||||||
var $tip = this.tip()
|
var $tip = this.tip()
|
||||||
var title = this.getTitle()
|
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')
|
$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
|
// TOOLTIP PLUGIN DEFINITION
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
Reference in New Issue
Block a user