diff --git a/js/common.js b/js/common.js index a33dfe1..c60bf75 100644 --- a/js/common.js +++ b/js/common.js @@ -40,21 +40,6 @@ var a2zString = ['a','b','c','d','e','f','g','h','i','j','k','l','m', supportedLanguages = ['de', 'es', 'fr', 'it', 'no', 'pl', 'pt', 'oc', 'ru', 'sl', 'zh'], mimeTypes = ['image/png', 'application/octet-stream'], formats = ['plaintext', 'markdown', 'syntaxhighlighting'], - /** - * character to HTML entity lookup table - * - * @see {@link https://github.com/janl/mustache.js/blob/master/mustache.js#L60} - */ - entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }, mimeFile = fs.createReadStream('/etc/mime.types'), mimeLine = ''; @@ -97,22 +82,6 @@ function parseMime(line) { exports.atob = atob; exports.btoa = btoa; -/** - * convert all applicable characters to HTML entities - * - * @see {@link https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content} - * @name htmlEntities - * @function - * @param {string} str - * @return {string} escaped HTML - */ -exports.htmlEntities = function(str) { - return String(str).replace( - /[&<>"'`=\/]/g, function(s) { - return entityMap[s]; - }); -}; - // provides random lowercase characters from a to z exports.jscA2zString = function() { return jsc.elements(a2zString); diff --git a/js/privatebin.js b/js/privatebin.js index dc7adef..66cc98e 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -440,7 +440,33 @@ jQuery.PrivateBin = (function($, RawDeflate) { expirationDate = expirationDate.setUTCSeconds(expirationDate.getUTCSeconds() + secondsToExpiration); return expirationDate; - } + }; + + /** + * encode all applicable characters to HTML entities + * + * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html} + * + * @name Helper.htmlEntities + * @function + * @param string str + * @return string escaped HTML + */ + me.htmlEntities = function(str) { + // using textarea, since other tags may allow and execute scripts, even when detached from DOM + let holder = document.createElement('textarea'); + holder.textContent = str; + // as per OWASP recommendation, also encoding quotes and slash + return holder.innerHTML.replace( + /["'\/]/g, + function(s) { + return { + '"': '"', + "'": ''', + '/': '/' + }[s]; + }); + }; return me; })(); @@ -592,16 +618,31 @@ jQuery.PrivateBin = (function($, RawDeflate) { args[0] = translations[messageId]; } + // messageID may contain links, but should be from a trusted source (code or translation JSON files) + let containsNoLinks = args[0].indexOf(' 0) may never contain HTML as they may come from untrusted parties + if (i > 0 || containsNoLinks) { + args[i] = Helper.htmlEntities(args[i]); + } + } + // format string let output = Helper.sprintf.apply(this, args); // if $element is given, apply text to element if ($element !== null) { - // avoid HTML entity encoding if translation contains link - if (output.indexOf('').text(text).html() + Helper.htmlEntities(text) ), sanitizedLinkedText = DOMPurify.sanitize(escapedLinkedText); $plainText.html(sanitizedLinkedText); @@ -2796,11 +2837,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { $attachmentLink.appendTo($element); // update text - ensuring no HTML is inserted into the text node - I18n._( - $attachmentLink, - $('
').text(label).html(), - $('').text($attachmentLink.attr('download')).html() - ); + I18n._($attachmentLink, label, $attachmentLink.attr('download')); }; /** @@ -3498,7 +3535,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { for (let i = 0; i < $head.length; ++i) { newDoc.write($head[i].outerHTML); } - newDoc.write('' + DOMPurify.sanitize($('').text(paste).html()) + '