CVE-2019-12313: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: XSS exists in Shave before 2.5.3 because output encoding is mishandled during the overwrite of an HTML element.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2019-05-24T14:29Z
Repository: https://github.com/dollarshaveclub/shave
Language: JavaScript
Files changed: dist/jquery.shave.min.js, dist/shave.es.js, dist/shave.js, dist/shave.min.js, package.json, src/shave.js
Commit date: 2019-04-16 22:49:10 -0700
Commit message: [version] patch; @digitalcraft createTextNode() 🚀 (#146)


Diff: diff --git a/dist/jquery.shave.min.js b/dist/jquery.shave.min.js
index e97cde5..61eca07 100644
--- a/dist/jquery.shave.min.js
+++ b/dist/jquery.shave.min.js
@@ -1 +1 @@
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";if("undefined"!=typeof window){var e=window.$||window.jQuery||window.Zepto;e&&(e.fn.shave=function(e,t){return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",s="boolean"!=typeof n.spaces||n.spaces,r='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var f=i[c],h=f.style,l=f.querySelector(".".concat(a)),d=void 0===f.textContent?"innerText":"textContent";l&&(f.removeChild(f.querySelector(".js-shave-char")),f[d]=f[d]);var v=f[d],g=s?v.split(" "):v;if(!(g.length<2)){var u=h.height;h.height="auto";var p=h.maxHeight;if(h.maxHeight="none",f.offsetHeight<=t)h.height=u,h.maxHeight=p;else{for(var y=g.length-1,j=0,m=void 0;j<y;)m=j+y+1>>1,f[d]=s?g.slice(0,m).join(" "):g.slice(0,m),f.insertAdjacentHTML("beforeend",r),f.offsetHeight>t?y=s?m-1:m-2:j=m;f[d]=s?g.slice(0,y).join(" "):g.slice(0,y),f.insertAdjacentHTML("beforeend",r);var H=s?" ".concat(g.slice(y).join(" ")):g.slice(y);f.insertAdjacentHTML("beforeend",'<span class="'.concat(a,'" style="display:none;">').concat(H,"</span>")),h.height=u,h.maxHeight=p}}}}}(this,e,t),this})}});
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";if("undefined"!=typeof window){var e=window.$||window.jQuery||window.Zepto;e&&(e.fn.shave=function(e,t){return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",r="boolean"!=typeof n.spaces||n.spaces,s='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var d=i[c],h=d.style,f=d.querySelector(".".concat(a)),l=void 0===d.textContent?"innerText":"textContent";f&&(d.removeChild(d.querySelector(".js-shave-char")),d[l]=d[l]);var u=d[l],v=r?u.split(" "):u;if(!(v.length<2)){var g=h.height;h.height="auto";var p=h.maxHeight;if(h.maxHeight="none",d.offsetHeight<=t)h.height=g,h.maxHeight=p;else{for(var m=v.length-1,y=0,j=void 0;y<m;)j=y+m+1>>1,d[l]=r?v.slice(0,j).join(" "):v.slice(0,j),d.insertAdjacentHTML("beforeend",s),d.offsetHeight>t?m=r?j-1:j-2:y=j;d[l]=r?v.slice(0,m).join(" "):v.slice(0,m),d.insertAdjacentHTML("beforeend",s);var x=r?" ".concat(v.slice(m).join(" ")):v.slice(m),w=document.createTextNode(x),H=document.createElement("span");H.classList.add(a),H.style.display="none",H.appendChild(w),d.insertAdjacentElement("beforeend",H),h.height=g,h.maxHeight=p}}}}}(this,e,t),this})}});
diff --git a/dist/shave.es.js b/dist/shave.es.js
index 08cd32d..0c38bfe 100644
--- a/dist/shave.es.js
+++ b/dist/shave.es.js
@@ -1,8 +1,8 @@
 /**
   shave - Shave is a javascript plugin that truncates multi-line text within a html element based on set max height
-  @version v2.5.2
+  @version v2.5.3
   @link https://github.com/dollarshaveclub/shave#readme
   @author Jeff Wainwright <yowainwright@gmail.com> (jeffry.in)
   @license MIT
 **/
 function shave(target, maxHeight) {
@@ -59,11 +59,16 @@ function shave(target, maxHeight) {
     }
 
     el[textProp] = spaces ? words.slice(0, max).join(' ') : words.slice(0, max);
     el.insertAdjacentHTML('beforeend', charHtml);
     var diff = spaces ? " ".concat(words.slice(max).join(' ')) : words.slice(max);
-    el.insertAdjacentHTML('beforeend', "<span class=\"".concat(classname, "\" style=\"display:none;\">").concat(diff, "</span>"));
+    var shavedText = document.createTextNode(diff);
+    var elWithShavedText = document.createElement('span');
+    elWithShavedText.classList.add(classname);
+    elWithShavedText.style.display = 'none';
+    elWithShavedText.appendChild(shavedText);
+    el.insertAdjacentElement('beforeend', elWithShavedText);
     styles.height = heightStyle;
     styles.maxHeight = maxHeightStyle;
   }
 }
 
diff --git a/dist/shave.js b/dist/shave.js
index ce9457c..101ef59 100644
--- a/dist/shave.js
+++ b/dist/shave.js
@@ -1,8 +1,8 @@
 /**
   shave - Shave is a javascript plugin that truncates multi-line text within a html element based on set max height
-  @version v2.5.2
+  @version v2.5.3
   @link https://github.com/dollarshaveclub/shave#readme
   @author Jeff Wainwright <yowainwright@gmail.com> (jeffry.in)
   @license MIT
 **/
 (function (global, factory) {
@@ -65,11 +65,16 @@
       }
 
       el[textProp] = spaces ? words.slice(0, max).join(' ') : words.slice(0, max);
       el.insertAdjacentHTML('beforeend', charHtml);
       var diff = spaces ? " ".concat(words.slice(max).join(' ')) : words.slice(max);
-      el.insertAdjacentHTML('beforeend', "<span class=\"".concat(classname, "\" style=\"display:none;\">").concat(diff, "</span>"));
+      var shavedText = document.createTextNode(diff);
+      var elWithShavedText = document.createElement('span');
+      elWithShavedText.classList.add(classname);
+      elWithShavedText.style.display = 'none';
+      elWithShavedText.appendChild(shavedText);
+      el.insertAdjacentElement('beforeend', elWithShavedText);
       styles.height = heightStyle;
       styles.maxHeight = maxHeightStyle;
     }
   }
 
diff --git a/dist/shave.min.js b/dist/shave.min.js
index c5c04db..0a45179 100644
--- a/dist/shave.min.js
+++ b/dist/shave.min.js
@@ -1 +1 @@
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.shave=t()}(this,function(){"use strict";return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",s="boolean"!=typeof n.spaces||n.spaces,r='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var h=i[c],l=h.style,f=h.querySelector(".".concat(a)),d=void 0===h.textContent?"innerText":"textContent";f&&(h.removeChild(h.querySelector(".js-shave-char")),h[d]=h[d]);var v=h[d],g=s?v.split(" "):v;if(!(g.length<2)){var p=l.height;l.height="auto";var u=l.maxHeight;if(l.maxHeight="none",h.offsetHeight<=t)l.height=p,l.maxHeight=u;else{for(var m=g.length-1,y=0,j=void 0;y<m;)j=y+m+1>>1,h[d]=s?g.slice(0,j).join(" "):g.slice(0,j),h.insertAdjacentHTML("beforeend",r),h.offsetHeight>t?m=s?j-1:j-2:y=j;h[d]=s?g.slice(0,m).join(" "):g.slice(0,m),h.insertAdjacentHTML("beforeend",r);var x=s?" ".concat(g.slice(m).join(" ")):g.slice(m);h.insertAdjacentHTML("beforeend",'<span class="'.concat(a,'" style="display:none;">').concat(x,"</span>")),l.height=p,l.maxHeight=u}}}}}});
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.shave=t()}(this,function(){"use strict";return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",r="boolean"!=typeof n.spaces||n.spaces,s='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var h=i[c],l=h.style,d=h.querySelector(".".concat(a)),f=void 0===h.textContent?"innerText":"textContent";d&&(h.removeChild(h.querySelector(".js-shave-char")),h[f]=h[f]);var v=h[f],g=r?v.split(" "):v;if(!(g.length<2)){var u=l.height;l.height="auto";var p=l.maxHeight;if(l.maxHeight="none",h.offsetHeight<=t)l.height=u,l.maxHeight=p;else{for(var m=g.length-1,x=0,y=void 0;x<m;)y=x+m+1>>1,h[f]=r?g.slice(0,y).join(" "):g.slice(0,y),h.insertAdjacentHTML("beforeend",s),h.offsetHeight>t?m=r?y-1:y-2:x=y;h[f]=r?g.slice(0,m).join(" "):g.slice(0,m),h.insertAdjacentHTML("beforeend",s);var j=r?" ".concat(g.slice(m).join(" ")):g.slice(m),H=document.createTextNode(j),b=document.createElement("span");b.classList.add(a),b.style.display="none",b.appendChild(H),h.insertAdjacentElement("beforeend",b),l.height=u,l.maxHeight=p}}}}}});
diff --git a/package.json b/package.json
index a5c680e..f3e7e6d 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "shave",
-  "version": "2.5.2",
+  "version": "2.5.3",
   "description": "Shave is a javascript plugin that truncates multi-line text within a html element based on set max height",
   "main": "dist/shave.js",
   "module": "dist/shave.es.js",
   "unpkg": "dist/shave.min.js",
   "files": [
diff --git a/src/shave.js b/src/shave.js
index cfc55f5..9741650 100644
--- a/src/shave.js
+++ b/src/shave.js
@@ -55,11 +55,10 @@ export default function shave (target, maxHeight, opts = {}) {
 
     el[textProp] = spaces ? words.slice(0, max).join(' ') : words.slice(0, max)
     el.insertAdjacentHTML('beforeend', charHtml)
     const diff = spaces ? ` ${words.slice(max).join(' ')}` : words.slice(max)
 
-    // https://stackoverflow.com/questions/476821/is-a-dom-text-node-guaranteed-to-not-be-interpreted-as-html
     const shavedText = document.createTextNode(diff)
     const elWithShavedText = document.createElement('span')
     elWithShavedText.classList.add(classname)
     elWithShavedText.style.display = 'none'
     elWithShavedText.appendChild(shavedText)

Vulnerable code: !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";if("undefined"!=typeof window){var e=window.$||window.jQuery||window.Zepto;e&&(e.fn.shave=function(e,t){return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",s="boolean"!=typeof n.spaces||n.spaces,r='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var f=i[c],h=f.style,l=f.querySelector(".".concat(a)),d=void 0===f.textContent?"innerText":"textContent";l&&(f.removeChild(f.querySelector(".js-shave-char")),f[d]=f[d]);var v=f[d],g=s?v.split(" "):v;if(!(g.length<2)){var u=h.height;h.height="auto";var p=h.maxHeight;if(h.maxHeight="none",f.offsetHeight<=t)h.height=u,h.maxHeight=p;else{for(var y=g.length-1,j=0,m=void 0;j<y;)m=j+y+1>>1,f[d]=s?g.slice(0,m).join(" "):g.slice(0,m),f.insertAdjacentHTML("beforeend",r),f.offsetHeight>t?y=s?m-1:m-2:j=m;f[d]=s?g.slice(0,y).join(" "):g.slice(0,y),f.insertAdjacentHTML("beforeend",r);var H=s?" ".concat(g.slice(y).join(" ")):g.slice(y);f.insertAdjacentHTML("beforeend",'<span class="'.concat(a,'" style="display:none;">').concat(H,"</span>")),h.height=u,h.maxHeight=p}}}}}(this,e,t),this})}});
  @version v2.5.2
    el.insertAdjacentHTML('beforeend', "<span class=\"".concat(classname, "\" style=\"display:none;\">").concat(diff, "</span>"));
  @version v2.5.2
      el.insertAdjacentHTML('beforeend', "<span class=\"".concat(classname, "\" style=\"display:none;\">").concat(diff, "</span>"));
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.shave=t()}(this,function(){"use strict";return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",s="boolean"!=typeof n.spaces||n.spaces,r='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var h=i[c],l=h.style,f=h.querySelector(".".concat(a)),d=void 0===h.textContent?"innerText":"textContent";f&&(h.removeChild(h.querySelector(".js-shave-char")),h[d]=h[d]);var v=h[d],g=s?v.split(" "):v;if(!(g.length<2)){var p=l.height;l.height="auto";var u=l.maxHeight;if(l.maxHeight="none",h.offsetHeight<=t)l.height=p,l.maxHeight=u;else{for(var m=g.length-1,y=0,j=void 0;y<m;)j=y+m+1>>1,h[d]=s?g.slice(0,j).join(" "):g.slice(0,j),h.insertAdjacentHTML("beforeend",r),h.offsetHeight>t?m=s?j-1:j-2:y=j;h[d]=s?g.slice(0,m).join(" "):g.slice(0,m),h.insertAdjacentHTML("beforeend",r);var x=s?" ".concat(g.slice(m).join(" ")):g.slice(m);h.insertAdjacentHTML("beforeend",'<span class="'.concat(a,'" style="display:none;">').concat(x,"</span>")),l.height=p,l.maxHeight=u}}}}}});
  "version": "2.5.2",
    // https://stackoverflow.com/questions/476821/is-a-dom-text-node-guaranteed-to-not-be-interpreted-as-html
Fixed code: !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";if("undefined"!=typeof window){var e=window.$||window.jQuery||window.Zepto;e&&(e.fn.shave=function(e,t){return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",r="boolean"!=typeof n.spaces||n.spaces,s='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var d=i[c],h=d.style,f=d.querySelector(".".concat(a)),l=void 0===d.textContent?"innerText":"textContent";f&&(d.removeChild(d.querySelector(".js-shave-char")),d[l]=d[l]);var u=d[l],v=r?u.split(" "):u;if(!(v.length<2)){var g=h.height;h.height="auto";var p=h.maxHeight;if(h.maxHeight="none",d.offsetHeight<=t)h.height=g,h.maxHeight=p;else{for(var m=v.length-1,y=0,j=void 0;y<m;)j=y+m+1>>1,d[l]=r?v.slice(0,j).join(" "):v.slice(0,j),d.insertAdjacentHTML("beforeend",s),d.offsetHeight>t?m=r?j-1:j-2:y=j;d[l]=r?v.slice(0,m).join(" "):v.slice(0,m),d.insertAdjacentHTML("beforeend",s);var x=r?" ".concat(v.slice(m).join(" ")):v.slice(m),w=document.createTextNode(x),H=document.createElement("span");H.classList.add(a),H.style.display="none",H.appendChild(w),d.insertAdjacentElement("beforeend",H),h.height=g,h.maxHeight=p}}}}}(this,e,t),this})}});
  @version v2.5.3
    var shavedText = document.createTextNode(diff);
    var elWithShavedText = document.createElement('span');
    elWithShavedText.classList.add(classname);
    elWithShavedText.style.display = 'none';
    elWithShavedText.appendChild(shavedText);
    el.insertAdjacentElement('beforeend', elWithShavedText);
  @version v2.5.3
      var shavedText = document.createTextNode(diff);
      var elWithShavedText = document.createElement('span');
      elWithShavedText.classList.add(classname);
      elWithShavedText.style.display = 'none';
      elWithShavedText.appendChild(shavedText);
      el.insertAdjacentElement('beforeend', elWithShavedText);
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.shave=t()}(this,function(){"use strict";return function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};if(!t)throw Error("maxHeight is required");var i="string"==typeof e?document.querySelectorAll(e):e;if(i){var o=n.character||"…",a=n.classname||"js-shave",r="boolean"!=typeof n.spaces||n.spaces,s='<span class="js-shave-char">'.concat(o,"</span>");"length"in i||(i=[i]);for(var c=0;c<i.length;c+=1){var h=i[c],l=h.style,d=h.querySelector(".".concat(a)),f=void 0===h.textContent?"innerText":"textContent";d&&(h.removeChild(h.querySelector(".js-shave-char")),h[f]=h[f]);var v=h[f],g=r?v.split(" "):v;if(!(g.length<2)){var u=l.height;l.height="auto";var p=l.maxHeight;if(l.maxHeight="none",h.offsetHeight<=t)l.height=u,l.maxHeight=p;else{for(var m=g.length-1,x=0,y=void 0;x<m;)y=x+m+1>>1,h[f]=r?g.slice(0,y).join(" "):g.slice(0,y),h.insertAdjacentHTML("beforeend",s),h.offsetHeight>t?m=r?y-1:y-2:x=y;h[f]=r?g.slice(0,m).join(" "):g.slice(0,m),h.insertAdjacentHTML("beforeend",s);var j=r?" ".concat(g.slice(m).join(" ")):g.slice(m),H=document.createTextNode(j),b=document.createElement("span");b.classList.add(a),b.style.display="none",b.appendChild(H),h.insertAdjacentElement("beforeend",b),l.height=u,l.maxHeight=p}}}}}});
  "version": "2.5.3",