diff options
| author | Johann-S <[email protected]> | 2019-02-11 16:59:39 +0200 |
|---|---|---|
| committer | XhmikosR <[email protected]> | 2019-02-13 08:32:15 +0200 |
| commit | 7bc4d2e0bc65151b6f60dccad50c9c8f50252bd6 (patch) | |
| tree | 178feb0626afeb5861d6c873f72efefc16e076ac /js/src | |
| parent | bf2515ae68f1d89e8b795478aec90f8db61159e5 (diff) | |
| download | bootstrap-7bc4d2e0bc65151b6f60dccad50c9c8f50252bd6.tar.xz bootstrap-7bc4d2e0bc65151b6f60dccad50c9c8f50252bd6.zip | |
Add sanitize template option for tooltip/popover plugins.
Diffstat (limited to 'js/src')
| -rw-r--r-- | js/src/tools/sanitizer.js | 127 | ||||
| -rw-r--r-- | js/src/tooltip.js | 59 |
2 files changed, 173 insertions, 13 deletions
diff --git a/js/src/tools/sanitizer.js b/js/src/tools/sanitizer.js new file mode 100644 index 000000000..00ed0d29e --- /dev/null +++ b/js/src/tools/sanitizer.js @@ -0,0 +1,127 @@ +/** + * -------------------------------------------------------------------------- + * Bootstrap (v4.3.0): tools/sanitizer.js + * 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 + +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 +} + +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 +} diff --git a/js/src/tooltip.js b/js/src/tooltip.js index 859ab918f..e7b5b2a7f 100644 --- a/js/src/tooltip.js +++ b/js/src/tooltip.js @@ -5,6 +5,10 @@ * -------------------------------------------------------------------------- */ +import { + DefaultWhitelist, + sanitizeHtml +} from './tools/sanitizer' import $ from 'jquery' import Popper from 'popper.js' import Util from './util' @@ -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', @@ -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 = { @@ -60,7 +68,10 @@ const Default = { offset : 0, container : false, fallbackPlacement : 'flip', - boundary : 'scrollParent' + boundary : 'scrollParent', + sanitize : true, + sanitizeFn : null, + whiteList : DefaultWhitelist } const HoverState = { @@ -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) } else { - $element[html ? 'html' : 'text'](content) + $element.text(content) } } @@ -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 : {} } @@ -663,6 +692,10 @@ class Tooltip { this.constructor.DefaultType ) + if (config.sanitize) { + config.template = sanitizeHtml(config.template, config.whiteList, config.sanitizeFn) + } + return config } |
