CVE-2019-12313
Description
Shave v2.5.2 and earlier are vulnerable to stored XSS due to unsafe use of innerHTML when overwriting truncated text.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Shave v2.5.2 and earlier are vulnerable to stored XSS due to unsafe use of innerHTML when overwriting truncated text.
Root
Cause CVE-2019-12313 is a cross-site scripting (XSS) vulnerability in the Shave JavaScript library before version 2.5.3. The library truncates multi-line text within an HTML element based on a set max height. In vulnerable versions, after calculating the truncated text (diff), the library directly injected this user-controlled content into the DOM using insertAdjacentHTML('beforeend', ...) with an innerHTML-like string. Because no output encoding was applied, any malicious HTML or script markup embedded in the truncated text would be interpreted and executed by the browser [1][2].
Attack
Surface To exploit this vulnerability, an attacker would need to influence the text content that Shave truncates. This could occur if the library is used to display user-generated content (e.g., comments, posts) or any string that includes attacker-controlled data. The attack requires no special privileges beyond the ability to provide content that is later truncated by Shave. No authentication is needed if the content is publicly viewable, and the attack is performed entirely client-side when a victim visits a page using the vulnerable Shave version [1][2].
The fix, introduced in commit da7371b, replaces the unsafe insertAdjacentHTML call with safe DOM methods: document.createTextNode() and appendChild(). This ensures the truncated text is treated as plain text and never parsed as HTML, completely eliminating the XSS vector [2][3].
Impact
A successful XSS attack could allow an attacker to execute arbitrary JavaScript in the context of the victim's browser. This could lead to session theft, credential harvesting, defacement, or redirection to malicious sites. The vulnerability is classified as moderate severity because it requires the library to operate on attacker-influenced text [1].
Mitigation
Users should upgrade Shave to version 2.5.3 or later. The official commit demonstrates the patch, and the repository has since been archived. No workaround is available—upgrading is the only recommended mitigation [2][3].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
shavenpm | < 2.5.3 | 2.5.3 |
Affected products
2- Shave/Shavedescription
Patches
1da7371b0531b[version] patch; @digitalcraft createTextNode() 🚀 (#146)
6 files changed · +17 −8
dist/jquery.shave.min.js+1 −1 modified@@ -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})}});
dist/shave.es.js+7 −2 modified@@ -1,6 +1,6 @@ /** 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 @@ -61,7 +61,12 @@ 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; }
dist/shave.js+7 −2 modified@@ -1,6 +1,6 @@ /** 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 @@ -67,7 +67,12 @@ 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; }
dist/shave.min.js+1 −1 modified@@ -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}}}}}});
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "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",
src/shave.js+0 −1 modified@@ -57,7 +57,6 @@ export default function shave (target, maxHeight, opts = {}) { 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)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-gh4g-3gm9-5wrqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-12313ghsaADVISORY
- github.com/dollarshaveclub/shave/commit/da7371b0531ba14eae48ef1bb1456a3de4cfa954ghsax_refsource_MISCWEB
- github.com/dollarshaveclub/shave/compare/852b537...da7371bghsax_refsource_MISCWEB
- www.npmjs.com/advisories/822ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.