summaryrefslogtreecommitdiff
path: root/web-ui/app/js
diff options
context:
space:
mode:
authorFelix Hammerl <fhammerl@thoughtworks.com>2016-02-24 10:13:25 +0100
committerFelix Hammerl <fhammerl@thoughtworks.com>2016-02-24 10:20:36 +0100
commit77ec41bb6f542077503106cacc1dbd28118c50b4 (patch)
tree7e59c1e5cdffd6146acb6504a9741d394af6a62f /web-ui/app/js
parent6160633ab9a54238974af3cf498024ad98fc977e (diff)
Issue #617: Sanitize received content
Sanitizes received HTML content with DOMPurify, making it safe for displaying and templating. Sanitizes received plain text content by encoding every single character as HTML entity.
Diffstat (limited to 'web-ui/app/js')
-rw-r--r--web-ui/app/js/helpers/sanitizer.js108
-rw-r--r--web-ui/app/js/helpers/view_helper.js37
-rw-r--r--web-ui/app/js/mail_view/ui/mail_view.js1
-rw-r--r--web-ui/app/js/main.js2
4 files changed, 115 insertions, 33 deletions
diff --git a/web-ui/app/js/helpers/sanitizer.js b/web-ui/app/js/helpers/sanitizer.js
new file mode 100644
index 00000000..eea1f0f7
--- /dev/null
+++ b/web-ui/app/js/helpers/sanitizer.js
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2016 ThoughtWorks, Inc.
+ *
+ * Pixelated is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Pixelated is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define(['DOMPurify', 'he'], function (DOMPurify, he) {
+ 'use strict';
+
+ /**
+ * Sanitizes a mail body to safe-to-display HTML
+ */
+ var sanitizer = {};
+
+ /**
+ * Adds html line breaks to a plaintext with line breaks (incl carriage return)
+ *
+ * @param {string} textPlainBody Plaintext input
+ * @returns {string} Plaintext with HTML line breals (<br/>)
+ */
+ sanitizer.addLineBreaks = function (textPlainBody) {
+ return textPlainBody.replace(/(\r)?\n/g, '<br/>').replace(/(&#xD;)?&#xA;/g, '<br/>');
+ };
+
+ /**
+ * Runs a given dirty body through DOMPurify, thereby removing
+ * potentially hazardous XSS attacks. Please be advised that this
+ * will not act as a privacy leak prevention. Contained contents
+ * will still point to remote sources.
+ *
+ * For future reference: Running DOMPurify with these parameters
+ * can help mitigate some of the most widely used privacy leaks.
+ * FORBID_TAGS: ['style', 'svg', 'audio', 'video', 'math'],
+ * FORBID_ATTR: ['src']
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyHtml = function (dirtyBody) {
+ return DOMPurify.sanitize(dirtyBody, {
+ SAFE_FOR_JQUERY: true,
+ SAFE_FOR_TEMPLATES: true
+ });
+ };
+
+ /**
+ * Runs a given dirty body through he, thereby encoding everything
+ * as HTML entities.
+ *
+ * @param {string} dirtyBody The unsanitized string
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.purifyText = function (dirtyBody) {
+ return he.encode(dirtyBody, {
+ encodeEverything: true
+ });
+ };
+
+ /**
+ * Calls #purify and #addLineBreaks to turn untrusted mail body content
+ * into safe-to-display HTML.
+ *
+ * NB: HTML content is preferred to plaintext content.
+ *
+ * @param {object} mail Pixelated Mail Object
+ * @return {string} Safe-to-display HTML string
+ */
+ sanitizer.sanitize = function (mail) {
+ var body;
+
+ if (mail.htmlBody) {
+ body = this.purifyHtml(mail.htmlBody);
+ } else {
+ body = this.purifyText(mail.textPlainBody);
+ body = this.addLineBreaks(body);
+ }
+
+ return body;
+ };
+
+ /**
+ * Add hooks to DOMPurify for opening links in new windows
+ */
+ DOMPurify.addHook('afterSanitizeAttributes', function (node) {
+ // set all elements owning target to target=_blank
+ if ('target' in node) {
+ node.setAttribute('target', '_blank');
+ }
+
+ // set non-HTML/MathML links to xlink:show=new
+ if (!node.hasAttribute('target') && (node.hasAttribute('xlink:href') || node.hasAttribute('href'))) {
+ node.setAttribute('xlink:show', 'new');
+ }
+ });
+
+ return sanitizer;
+});
diff --git a/web-ui/app/js/helpers/view_helper.js b/web-ui/app/js/helpers/view_helper.js
index e4e9277d..e8d517a5 100644
--- a/web-ui/app/js/helpers/view_helper.js
+++ b/web-ui/app/js/helpers/view_helper.js
@@ -17,12 +17,12 @@
define(
[
'helpers/contenttype',
- 'lib/html_whitelister',
'views/i18n',
'quoted-printable/quoted-printable',
- 'utf8/utf8'
+ 'utf8/utf8',
+ 'helpers/sanitizer'
],
- function(contentType, htmlWhitelister, i18n, quotedPrintable, utf8) {
+ function(contentType, i18n, quotedPrintable, utf8, sanitizer) {
'use strict';
function formatStatusClasses(ss) {
@@ -31,37 +31,8 @@ define(
}).join(' ');
}
- function addParagraphsToPlainText(textPlainBody) {
- return textPlainBody.replace(/^(.*?)$/mg, '$1<br/>');
- }
-
- function escapeHtmlTags(body) {
-
- var escapeIndex = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- '\'':'&#39;',
- '/': '&#x2f;'
- };
-
- return body.replace(/["'<>\/&]/g, function(char){
- return escapeIndex[char];
- });
-
- }
-
- function escapeHtmlAndAddParagraphs(body) {
- var escapedBody = escapeHtmlTags(body);
- return addParagraphsToPlainText(escapedBody);
- }
-
function formatMailBody(mail) {
- var body = mail.htmlBody ?
- htmlWhitelister.sanitize(mail.htmlBody, htmlWhitelister.tagPolicy) :
- escapeHtmlAndAddParagraphs(mail.textPlainBody);
- return $('<div>' + body + '</div>');
+ return sanitizer.sanitize(mail);
}
function moveCaretToEnd(el) {
diff --git a/web-ui/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js
index d4f5dd9e..8465b45a 100644
--- a/web-ui/app/js/mail_view/ui/mail_view.js
+++ b/web-ui/app/js/mail_view/ui/mail_view.js
@@ -72,6 +72,7 @@ define(
}));
this.$node.find('.bodyArea').html(viewHelpers.formatMailBody(data.mail));
+
this.trigger(document, events.search.highlightResults, {where: '.bodyArea'});
this.trigger(document, events.search.highlightResults, {where: '.subjectArea'});
this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'});
diff --git a/web-ui/app/js/main.js b/web-ui/app/js/main.js
index 5fb2e46f..e093e790 100644
--- a/web-ui/app/js/main.js
+++ b/web-ui/app/js/main.js
@@ -22,6 +22,8 @@ requirejs.config({
'page': 'js/page',
'feedback': 'js/feedback',
'flight': 'bower_components/flight',
+ 'DOMPurify': 'bower_components/DOMPurify/dist/purify.min',
+ 'he': 'bower_components/he/he',
'hbs': 'js/generated/hbs',
'helpers': 'js/helpers',
'lib': 'js/lib',