Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sanitize template option for tooltip/popover plugins #28236

Merged
merged 3 commits into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions js/src/tools/sanitizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* --------------------------------------------------------------------------
* Bootstrap (v4.3.0): tools/sanitizer.js
Johann-S marked this conversation as resolved.
Show resolved Hide resolved
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* --------------------------------------------------------------------------
*/

const uriAttrs = [
'background',
'cite',
'href',
'itemtype',
'longdesc',
'poster',
'src',
'xlink:href'
]

const ARIA_ATTRIBUTE_PATTERN = /^aria-[\w-]*$/i
Johann-S marked this conversation as resolved.
Show resolved Hide resolved

export const 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
*/
const 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
*/
const 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 (allowedAttributeList.indexOf(attrName) !== -1) {
if (uriAttrs.indexOf(attrName) !== -1) {
return Boolean(attr.nodeValue.match(SAFE_URL_PATTERN) || attr.nodeValue.match(DATA_URL_PATTERN))
}

return true
}

const regExp = allowedAttributeList.filter((attrRegex) => attrRegex instanceof RegExp)

// Check if a regular expression validates the attribute.
for (let i = 0, l = regExp.length; i < l; i++) {
if (attrName.match(regExp[i])) {
return true
}
}

return false
Johann-S marked this conversation as resolved.
Show resolved Hide resolved
}

export function sanitizeHtml(unsafeHtml, whiteList, sanitizeFn) {
if (unsafeHtml.length === 0) {
return unsafeHtml
}

if (sanitizeFn && typeof sanitizeFn === 'function') {
return sanitizeFn(unsafeHtml)
}

const domParser = new window.DOMParser()
const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html')
const whitelistKeys = Object.keys(whiteList)
const elements = [].slice.call(createdDocument.body.querySelectorAll('*'))

for (let i = 0, len = elements.length; i < len; i++) {
const el = elements[i]
const elName = el.nodeName.toLowerCase()

if (whitelistKeys.indexOf(el.nodeName.toLowerCase()) === -1) {
el.parentNode.removeChild(el)

continue
}

const attributeList = [].slice.call(el.attributes)
const whitelistedAttributes = [].concat(whiteList['*'] || [], whiteList[elName] || [])

attributeList.forEach((attr) => {
if (!allowedAttribute(attr, whitelistedAttributes)) {
el.removeAttribute(attr.nodeName)
}
})
}

return createdDocument.body.innerHTML
}
59 changes: 46 additions & 13 deletions js/src/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
* --------------------------------------------------------------------------
*/

import {
DefaultWhitelist,
sanitizeHtml
} from './tools/sanitizer'
import $ from 'jquery'
import Popper from 'popper.js'
import Util from './util'
Expand All @@ -15,13 +19,14 @@ import Util from './util'
* ------------------------------------------------------------------------
*/

const NAME = 'tooltip'
const VERSION = '4.3.0'
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const JQUERY_NO_CONFLICT = $.fn[NAME]
const CLASS_PREFIX = 'bs-tooltip'
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const NAME = 'tooltip'
const VERSION = '4.3.0'
const DATA_KEY = 'bs.tooltip'
const EVENT_KEY = `.${DATA_KEY}`
const JQUERY_NO_CONFLICT = $.fn[NAME]
const CLASS_PREFIX = 'bs-tooltip'
const BSCLS_PREFIX_REGEX = new RegExp(`(^|\\s)${CLASS_PREFIX}\\S+`, 'g')
const DISALLOWED_ATTRIBUTES = ['sanitize', 'whiteList', 'sanitizeFn']

const DefaultType = {
animation : 'boolean',
Expand All @@ -35,7 +40,10 @@ const DefaultType = {
offset : '(number|string|function)',
container : '(string|element|boolean)',
fallbackPlacement : '(string|array)',
boundary : '(string|element)'
boundary : '(string|element)',
sanitize : 'boolean',
sanitizeFn : '(null|function)',
whiteList : 'object'
}

const AttachmentMap = {
Expand All @@ -60,7 +68,10 @@ const Default = {
offset : 0,
container : false,
fallbackPlacement : 'flip',
boundary : 'scrollParent'
boundary : 'scrollParent',
sanitize : true,
sanitizeFn : null,
whiteList : DefaultWhitelist
}

const HoverState = {
Expand Down Expand Up @@ -419,18 +430,27 @@ class Tooltip {
}

setElementContent($element, content) {
const html = this.config.html
if (typeof content === 'object' && (content.nodeType || content.jquery)) {
// Content is a DOM node or a jQuery
if (html) {
if (this.config.html) {
if (!$(content).parent().is($element)) {
$element.empty().append(content)
}
} else {
$element.text($(content).text())
}

return
}

if (this.config.html) {
if (this.config.sanitize) {
content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn)
}

$element.html(content)
Johann-S marked this conversation as resolved.
Show resolved Hide resolved
} else {
$element[html ? 'html' : 'text'](content)
$element.text(content)
}
}

Expand Down Expand Up @@ -636,9 +656,18 @@ class Tooltip {
}

_getConfig(config) {
const dataAttributes = $(this.element).data()

Object.keys(dataAttributes)
.forEach((dataAttr) => {
if (DISALLOWED_ATTRIBUTES.indexOf(dataAttr) !== -1) {
delete dataAttributes[dataAttr]
}
})

config = {
...this.constructor.Default,
...$(this.element).data(),
...dataAttributes,
...typeof config === 'object' && config ? config : {}
}

Expand All @@ -663,6 +692,10 @@ class Tooltip {
this.constructor.DefaultType
)

if (config.sanitize) {
config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn)
}

return config
}

Expand Down
Loading