Skip to content

Commit

Permalink
Allow most HTML tags but sanitize for potential XSS issues
Browse files Browse the repository at this point in the history
Fixes #107
  • Loading branch information
nebulade committed Jun 30, 2021
1 parent 3f1df35 commit da151c0
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 1 deletion.
111 changes: 111 additions & 0 deletions 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 == "<br>") 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("<body></body>"); // 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(/<br[^>]*>(\S)/g, "<br>\n$1")
.replace(/div><div/g, "div>\n<div"); //replace is just for cleaner code
}

function startsWithAny(str, substrings) {
for (var i = 0; i < substrings.length; i++) {
if (str.indexOf(substrings[i]) == 0) {
return true;
}
}
return false;
}

this.AllowedTags = tagWhitelist_;
this.AllowedAttributes = attributeWhitelist_;
this.AllowedCssStyles = cssWhitelist_;
this.AllowedSchemas = schemaWhiteList_;
});
3 changes: 3 additions & 0 deletions frontend/index.html
Expand Up @@ -87,6 +87,9 @@ <h3 style="margin-top: 0">Archive</h3>
<script type="text/javascript" src="/3rdparty/js/markdown-it-emoji.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/superagent.min.js?<%= revision %>"></script>

<!-- https://github.com/jitbit/HtmlSanitizer -->
<script type="text/javascript" src="/3rdparty/js/HtmlSanitizer.js?<%= revision %>"></script>

<script type="text/javascript" src="/js/core.js?<%= revision %>"></script>

<%- include('templates/navigation-bar.html') %>
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/util.js
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/shared.html
Expand Up @@ -77,6 +77,9 @@ <h2 v-cloak>{{ publicProfile.username }}</h2>
<script type="text/javascript" src="/3rdparty/js/markdown-it-emoji.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/superagent.min.js?<%= revision %>"></script>

<!-- https://github.com/jitbit/HtmlSanitizer -->
<script type="text/javascript" src="/3rdparty/js/HtmlSanitizer.js?<%= revision %>"></script>

<script type="text/javascript" src="/js/core.js?<%= revision %>"></script>
<script type="text/javascript" src="/js/util.js?<%= revision %>"></script>
<script type="text/javascript" src="/js/shared.js?<%= revision %>"></script>
Expand Down
3 changes: 3 additions & 0 deletions frontend/stream.html
Expand Up @@ -78,6 +78,9 @@ <h2 v-cloak>{{ publicProfile.username }}</h2>
<script type="text/javascript" src="/3rdparty/js/markdown-it-emoji.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/superagent.min.js?<%= revision %>"></script>

<!-- https://github.com/jitbit/HtmlSanitizer -->
<script type="text/javascript" src="/3rdparty/js/HtmlSanitizer.js?<%= revision %>"></script>

<script type="text/javascript" src="/js/core.js?<%= revision %>"></script>
<script type="text/javascript" src="/js/util.js?<%= revision %>"></script>
<script type="text/javascript" src="/js/stream.js?<%= revision %>"></script>
Expand Down

0 comments on commit da151c0

Please sign in to comment.