From da151c0b9e09bb53193363f09fc1c0090e2384c2 Mon Sep 17 00:00:00 2001 From: Johannes Zellner Date: Wed, 30 Jun 2021 19:45:48 +0200 Subject: [PATCH] Allow most HTML tags but sanitize for potential XSS issues Fixes #107 --- frontend/3rdparty/js/HtmlSanitizer.js | 111 ++++++++++++++++++++++++++ frontend/index.html | 3 + frontend/js/util.js | 2 +- frontend/shared.html | 3 + frontend/stream.html | 3 + 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 frontend/3rdparty/js/HtmlSanitizer.js diff --git a/frontend/3rdparty/js/HtmlSanitizer.js b/frontend/3rdparty/js/HtmlSanitizer.js new file mode 100644 index 0000000..2ad8388 --- /dev/null +++ b/frontend/3rdparty/js/HtmlSanitizer.js @@ -0,0 +1,111 @@ +//JavaScript HTML Sanitizer, (c) Alexander Yumashev, Jitbit Software. + +//homepage https://github.com/jitbit/HtmlSanitizer + +//License: MIT https://github.com/jitbit/HtmlSanitizer/blob/master/LICENSE + +console.log('Sanitizer loading'); + +var HtmlSanitizer = new (function () { + + var tagWhitelist_ = { + 'A': true, 'ABBR': true, 'B': true, 'BLOCKQUOTE': true, 'BODY': true, 'BR': true, 'CENTER': true, 'CODE': true, 'DIV': true, 'EM': true, 'FONT': true, + 'H1': true, 'H2': true, 'H3': true, 'H4': true, 'H5': true, 'H6': true, 'HR': true, 'I': true, 'IMG': true, 'LABEL': true, 'LI': true, 'OL': true, 'P': true, 'PRE': true, + 'SMALL': true, 'SOURCE': true, 'SPAN': true, 'STRONG': true, 'TABLE': true, 'TBODY': true, 'TR': true, 'TD': true, 'TH': true, 'THEAD': true, 'UL': true, 'U': true, 'VIDEO': true + }; + + var contentTagWhiteList_ = { 'FORM': true }; //tags that will be converted to DIVs + + var attributeWhitelist_ = { 'align': true, 'color': true, 'controls': true, 'height': true, 'href': true, 'src': true, 'style': true, 'target': true, 'title': true, 'type': true, 'width': true }; + + var cssWhitelist_ = { 'color': true, 'background-color': true, 'font-size': true, 'text-align': true, 'text-decoration': true, 'font-weight': true }; + + var schemaWhiteList_ = [ 'http:', 'https:', 'data:', 'm-files:', 'file:', 'ftp:' ]; //which "protocols" are allowed in "href", "src" etc + + var uriAttributes_ = { 'href': true, 'action': true }; + + this.SanitizeHtml = function(input) { + input = input.trim(); + if (input == "") return ""; //to save performance and not create iframe + + //firefox "bogus node" workaround + if (input == "
") return ""; + + var iframe = document.createElement('iframe'); + if (iframe['sandbox'] === undefined) { + alert('Your browser does not support sandboxed iframes. Please upgrade to a modern browser.'); + return ''; + } + iframe['sandbox'] = 'allow-same-origin'; + iframe.style.display = 'none'; + document.body.appendChild(iframe); // necessary so the iframe contains a document + var iframedoc = iframe.contentDocument || iframe.contentWindow.document; + if (iframedoc.body == null) iframedoc.write(""); // null in IE + iframedoc.body.innerHTML = input; + + function makeSanitizedCopy(node) { + if (node.nodeType == Node.TEXT_NODE) { + var newNode = node.cloneNode(true); + } else if (node.nodeType == Node.ELEMENT_NODE && (tagWhitelist_[node.tagName] || contentTagWhiteList_[node.tagName])) { + + //remove useless empty spans (lots of those when pasting from MS Outlook) + if ((node.tagName == "SPAN" || node.tagName == "B" || node.tagName == "I" || node.tagName == "U") + && node.innerHTML.trim() == "") { + return document.createDocumentFragment(); + } + + if (contentTagWhiteList_[node.tagName]) + newNode = iframedoc.createElement('DIV'); //convert to DIV + else + newNode = iframedoc.createElement(node.tagName); + + for (var i = 0; i < node.attributes.length; i++) { + var attr = node.attributes[i]; + if (attributeWhitelist_[attr.name]) { + if (attr.name == "style") { + for (s = 0; s < node.style.length; s++) { + var styleName = node.style[s]; + if (cssWhitelist_[styleName]) + newNode.style.setProperty(styleName, node.style.getPropertyValue(styleName)); + } + } + else { + if (uriAttributes_[attr.name]) { //if this is a "uri" attribute, that can have "javascript:" or something + if (attr.value.indexOf(":") > -1 && !startsWithAny(attr.value, schemaWhiteList_)) + continue; + } + newNode.setAttribute(attr.name, attr.value); + } + } + } + for (i = 0; i < node.childNodes.length; i++) { + var subCopy = makeSanitizedCopy(node.childNodes[i]); + newNode.appendChild(subCopy, false); + } + } else { + newNode = document.createDocumentFragment(); + } + return newNode; + }; + + var resultElement = makeSanitizedCopy(iframedoc.body); + document.body.removeChild(iframe); + return resultElement.innerHTML + .replace(/]*>(\S)/g, "
\n$1") + .replace(/div>
\nArchive + + + <%- include('templates/navigation-bar.html') %> diff --git a/frontend/js/util.js b/frontend/js/util.js index 9c4dd08..05254f8 100644 --- a/frontend/js/util.js +++ b/frontend/js/util.js @@ -181,7 +181,7 @@ md.renderer.rules.emoji = function(token, idx) { Vue.filter('markdown', function (value) { if (!value) return ''; - return md.render(value); + return HtmlSanitizer.SanitizeHtml(md.render(value)); }); Vue.filter('prettyDateOffset', function (time) { diff --git a/frontend/shared.html b/frontend/shared.html index 0b6592b..542ccf0 100644 --- a/frontend/shared.html +++ b/frontend/shared.html @@ -77,6 +77,9 @@

{{ publicProfile.username }}

+ + + diff --git a/frontend/stream.html b/frontend/stream.html index 2d5f32d..892db59 100644 --- a/frontend/stream.html +++ b/frontend/stream.html @@ -78,6 +78,9 @@

{{ publicProfile.username }}

+ + +