From 3aae4cc9bb2b58c337bf25d2f04f129a2a0fa78f Mon Sep 17 00:00:00 2001 From: Sam Schurter Date: Thu, 19 Nov 2020 13:27:33 -0600 Subject: [PATCH] fixes vulnerabilities in #38 --- jade/page-contents/autocomplete_content.html | 8 + jade/page-contents/toasts_content.html | 51 ++++- jade/page-contents/tooltips_content.html | 26 ++- js/autocomplete.js | 57 +++--- js/toasts.js | 41 ++-- js/tooltip.js | 19 +- test/html/autocomplete.html | 195 +++++++++++++++++++ test/html/toast.html | 58 ++++++ test/html/tooltip.html | 42 ++++ 9 files changed, 440 insertions(+), 57 deletions(-) create mode 100644 test/html/autocomplete.html create mode 100644 test/html/toast.html create mode 100644 test/html/tooltip.html diff --git a/jade/page-contents/autocomplete_content.html b/jade/page-contents/autocomplete_content.html index 17bd59f122..b04c8699f4 100644 --- a/jade/page-contents/autocomplete_content.html +++ b/jade/page-contents/autocomplete_content.html @@ -39,6 +39,8 @@

Initialization

The data is a json object where the key is the matching string and the value is an optional image url.

+

The key must be a text string. If you trust your data, or have properly sanitized your user input, you may + use HTML by setting the option allowUnsafeHTML: true.


   document.addEventListener('DOMContentLoaded', function() {
     var elems = document.querySelectorAll('.autocomplete');
@@ -112,6 +114,12 @@ 

Options

Sort function that defines the order of the list of autocomplete options. + + allowUnsafeHTML + Boolean + false + If true will render the key from each item directly as HTML. User input MUST be properly sanitized first. + diff --git a/jade/page-contents/toasts_content.html b/jade/page-contents/toasts_content.html index 1b5ee48c0c..48422f46ea 100644 --- a/jade/page-contents/toasts_content.html +++ b/jade/page-contents/toasts_content.html @@ -4,14 +4,14 @@

Materialize provides an easy way for you to send unobtrusive alerts to your users through toasts. These toasts are also placed and sized responsively, try it out by clicking the button below on different device sizes.

- Toast! + Toast!

To do this, call the M.toast() function programmatically in JavaScript.


-  M.toast({html: 'I am a toast!'})
+  M.toast({text: 'I am a toast!'})
         

One way to add this into your application is to add this as an onclick event to a button.


-  <a onclick="M.toast({html: 'I am a toast'})" class="btn">Toast!</a>
+  <a onclick="M.toast({text: 'I am a toast'})" class="btn">Toast!</a>
         
@@ -30,11 +30,37 @@

Options

+ + text + String + '' + The content of the Toast. + + + unsafeHTML + String, HTMLElement + '' + + HTML content that will be appended to to text. + Only use properly sanitized or otherwise trusted data for unsafeHTML. + + html String '' - The HTML content of the Toast. + +

+ (DEPRECATED): will be removed in a later release. +

+

+ HTML content that will be appended to text. + Only use properly sanitized or otherwise trusted data for html. +

+

+ Will be ignored if unsafeHTML is set. +

+ displayLength @@ -117,11 +143,16 @@

Properties

Custom HTML

-

You can pass in an HTML String as the first argument as well. Take a look at the example below, where we pass in text as well as a flat button. If you call an external function instead of in-line JavaScript, you will not need to escape quotation marks.

+

You can pass in an HTML String as the first argument as well. Take a look at the example below, where we pass + in text as well as a flat button. If you call an external function instead of in-line JavaScript, you will not + need to escape quotation marks.

+

+ Only use a properly sanitized or otherwise trusted HTML string. +

Toast with Action

   var toastHTML = '<span>I am toast content</span><button class="btn-flat toast-action">Undo</button>';
-  M.toast({html: toastHTML});
+  M.toast({unsafeHTML: toastHTML});
         
@@ -129,9 +160,9 @@

Custom HTML

Callback

You can have the toast callback a function when it has been dismissed.

- Toast! + Toast!

-  <a class="btn" onclick="M.toast({html: 'I am a toast', completeCallback: function(){alert('Your toast was dismissed')}})">Toast!</a>
+  <a class="btn" onclick="M.toast({text: 'I am a toast', completeCallback: function(){alert('Your toast was dismissed')}})">Toast!</a>
         
@@ -140,11 +171,11 @@

Callback

Styling Toasts

We've added the ability to customize your toasts easily. You can pass in classes as an optional parameter into the toast function. We've added a rounded class for you, but you can create your own CSS classes and apply them to toasts. Checkout out our full example below.

- Round Toast! + Round Toast!

   // 'rounded' is the class I'm applying to the toast
-  M.toast({html: 'I am a toast!', classes: 'rounded'});
+  M.toast({text: 'I am a toast!', classes: 'rounded'});
         
diff --git a/jade/page-contents/tooltips_content.html b/jade/page-contents/tooltips_content.html index 404d5aff76..7feae0ff7a 100644 --- a/jade/page-contents/tooltips_content.html +++ b/jade/page-contents/tooltips_content.html @@ -70,11 +70,35 @@

Options

0 Delay time before tooltip appears. + + text + String + + Text string for the tooltip. + + + unsafeHTML + String + null + HTML content that will be appended to to text. + Only use properly sanitized or otherwise trusted data for unsafeHTML. + html String null - Can take regular text or HTML strings. + +

+ (DEPRECATED): will be removed in a later release. +

+

+ HTML content that will be appended to text. + Only use properly sanitized or otherwise trusted data for html. +

+

+ Will be ignored if unsafeHTML is set. +

+ margin diff --git a/js/autocomplete.js b/js/autocomplete.js index 1bed2301a2..5523c7522d 100644 --- a/js/autocomplete.js +++ b/js/autocomplete.js @@ -9,7 +9,8 @@ sortFunction: function(a, b, inputString) { // Sort function for sorting autocomplete results return a.indexOf(inputString) - b.indexOf(inputString); - } + }, + allowUnsafeHTML: false }; /** @@ -282,22 +283,14 @@ /** * Highlight partial match */ - _highlight(string, $el) { - let img = $el.find('img'); - let matchStart = $el - .text() - .toLowerCase() - .indexOf('' + string.toLowerCase() + ''), - matchEnd = matchStart + string.length - 1, - beforeMatch = $el.text().slice(0, matchStart), - matchText = $el.text().slice(matchStart, matchEnd + 1), - afterMatch = $el.text().slice(matchEnd + 1); - $el.html( - `${beforeMatch}${matchText}${afterMatch}` - ); - if (img.length) { - $el.prepend(img); - } + _highlight(input, label) { + const start = label.toLowerCase().indexOf('' + input.toLowerCase() + ''); + const end = start + input.length - 1; + //custom filters may return results where the string does not match any part + if (start == -1 || end == -1) { + return [label, '', '']; + } + return [label.slice(0, start), label.slice(start, end + 1), label.slice(end + 1)]; } /** @@ -376,18 +369,32 @@ // Render for (let i = 0; i < matchingData.length; i++) { - let entry = matchingData[i]; - let $autocompleteOption = $('
  • '); + const entry = matchingData[i]; + const item = document.createElement('li'); if (!!entry.data) { - $autocompleteOption.append( - `${entry.key}` - ); + const img = document.createElement('img'); + img.classList.add("right", "circle"); + img.src = entry.data; + item.appendChild(img); + } + + const parts = this._highlight(val, entry.key); + const s = document.createElement('span'); + if (this.options.allowUnsafeHTML) { + s.innerHTML = parts[0] + '' + parts[1] + '' + parts[2]; } else { - $autocompleteOption.append('' + entry.key + ''); + s.appendChild(document.createTextNode(parts[0])) + if (!!parts[1]){ + const highlight = document.createElement('span'); + highlight.textContent = parts[1]; + highlight.classList.add("highlight"); + s.appendChild(highlight); + s.appendChild(document.createTextNode(parts[2])); + } } + item.appendChild(s); - $(this.container).append($autocompleteOption); - this._highlight(val, $autocompleteOption); + $(this.container).append(item); } } diff --git a/js/toasts.js b/js/toasts.js index b0e4b83aa2..7500b285dd 100644 --- a/js/toasts.js +++ b/js/toasts.js @@ -3,6 +3,8 @@ let _defaults = { html: '', + unsafeHTML: '', + text: '', displayLength: 4000, inDuration: 300, outDuration: 375, @@ -18,7 +20,12 @@ * @member Toast#options */ this.options = $.extend({}, Toast.defaults, options); - this.message = this.options.html; + this.htmlMessage = this.options.html; + // If the new unsafeHTML is used, prefer that + if (!!this.options.unsafeHTML){ + this.htmlMessage = this.options.unsafeHTML; + } + this.message = this.options.text; /** * Describes current pan state toast @@ -188,28 +195,26 @@ $(toast).addClass(this.options.classes); } - // Set content + // Set safe text content + toast.textContent = this.message; if ( typeof HTMLElement === 'object' - ? this.message instanceof HTMLElement - : this.message && - typeof this.message === 'object' && - this.message !== null && - this.message.nodeType === 1 && - typeof this.message.nodeName === 'string' - ) { - toast.appendChild(this.message); - - // Check if it is jQuery object - } else if (!!this.message.jquery) { - $(toast).append(this.message[0]); - - // Insert as html; + ? this.htmlMessage instanceof HTMLElement + : this.htmlMessage && + typeof this.htmlMessage === 'object' && + this.htmlMessage !== null && + this.htmlMessage.nodeType === 1 && + typeof this.htmlMessage.nodeName === 'string' + ) { //if the htmlMessage is an HTML node, append it directly + toast.appendChild(this.htmlMessage); + } else if (!!this.htmlMessage.jquery) { // Check if it is jQuery object, append the node + $(toast).append(this.htmlMessage[0]); } else { - toast.innerHTML = this.message; + // Append as unsanitized html; + $(toast).append(this.htmlMessage); } - // Append toasft + // Append toast Toast._container.appendChild(toast); return toast; } diff --git a/js/tooltip.js b/js/tooltip.js index 1769597fb5..a14e8f3f6b 100644 --- a/js/tooltip.js +++ b/js/tooltip.js @@ -5,6 +5,8 @@ exitDelay: 200, enterDelay: 0, html: null, + text: '', + unsafeHTML: null, margin: 5, inDuration: 250, outDuration: 200, @@ -68,13 +70,24 @@ let tooltipContentEl = document.createElement('div'); tooltipContentEl.classList.add('tooltip-content'); - tooltipContentEl.innerHTML = this.options.html; + this._setTooltipContent(tooltipContentEl); + tooltipEl.appendChild(tooltipContentEl); document.body.appendChild(tooltipEl); } + _setTooltipContent(tooltipContentEl) { + tooltipContentEl.textContent = this.options.text; + if (!!this.options.html){ + $(tooltipContentEl).append(this.options.html); + } + if (!!this.options.unsafeHTML){ + $(tooltipContentEl).append(this.options.unsafeHTML); + } + } + _updateTooltipContent() { - this.tooltipEl.querySelector('.tooltip-content').innerHTML = this.options.html; + this._setTooltipContent(this.tooltipEl.querySelector('.tooltip-content')); } _setupEventHandlers() { @@ -285,7 +298,7 @@ let positionOption = this.el.getAttribute('data-position'); if (tooltipTextOption) { - attributeOptions.html = tooltipTextOption; + attributeOptions.text = tooltipTextOption; } if (positionOption) { diff --git a/test/html/autocomplete.html b/test/html/autocomplete.html new file mode 100644 index 0000000000..ba33043b27 --- /dev/null +++ b/test/html/autocomplete.html @@ -0,0 +1,195 @@ + + + + + + + Materialize - Documentation + + + + + + + + + + + + + + + + + + + +
    + +
    Default autocomplete example
    +
    +
    +
    +
    + textsms + + +
    +
    +
    +
    + +
    Custom filter function
    +
    +
    +
    +
    + textsms + + +
    +
    +
    +
    + +
    Limit Set
    +
    +
    +
    +
    + textsms + + +
    +
    +
    +
    + +
    allowUnsafeHTML: false
    +
    +
    +
    +
    + textsms + + +
    +
    +
    +
    + +
    allowUnsafeHTML: true
    +
    +
    +
    +
    + textsms + + +
    +
    +
    +
    + + + +
    + + + + + + + + \ No newline at end of file diff --git a/test/html/toast.html b/test/html/toast.html new file mode 100644 index 0000000000..2fdf2c073c --- /dev/null +++ b/test/html/toast.html @@ -0,0 +1,58 @@ + + + + + + + Materialize - Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/html/tooltip.html b/test/html/tooltip.html new file mode 100644 index 0000000000..19767e7cba --- /dev/null +++ b/test/html/tooltip.html @@ -0,0 +1,42 @@ + + + + + + + Materialize - Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file