changeset bea22c5b2072 in sao:5.6
details: https://hg.tryton.org/sao?cmd=changeset;node=bea22c5b2072
description:
        Sanitize RichtText fields content

        issue9405
        review327451002
        (grafted from 4e0e93b11cad63c6b25f5230055653edb21a334c)
diffstat:

 CHANGELOG             |    1 +
 COPYRIGHT             |    1 +
 Gruntfile.js          |    3 +-
 src/html_sanitizer.js |  105 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/view/form.js      |    5 +-
 tests/sao.js          |   19 +++++++++
 6 files changed, 131 insertions(+), 3 deletions(-)

diffs (192 lines):

diff -r 6d7c2dbd02a4 -r bea22c5b2072 CHANGELOG
--- a/CHANGELOG Mon Jun 29 17:29:45 2020 +0200
+++ b/CHANGELOG Mon Jun 29 17:33:06 2020 +0200
@@ -1,3 +1,4 @@
+* Sanitize RichtText fields content (issue9405)
 * Escape external string (issue9394)
 
 Version 5.6.3 - 2020-06-16
diff -r 6d7c2dbd02a4 -r bea22c5b2072 COPYRIGHT
--- a/COPYRIGHT Mon Jun 29 17:29:45 2020 +0200
+++ b/COPYRIGHT Mon Jun 29 17:33:06 2020 +0200
@@ -2,6 +2,7 @@
 Copyright (C) 2012-2020 Cédric Krier.
 Copyright (C) 2012-2014 Bertrand Chenal.
 Copyright (C) 2012-2020 B2CK SPRL.
+Copyright (C) 2019 Jitbit.
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
diff -r 6d7c2dbd02a4 -r bea22c5b2072 Gruntfile.js
--- a/Gruntfile.js      Mon Jun 29 17:29:45 2020 +0200
+++ b/Gruntfile.js      Mon Jun 29 17:33:06 2020 +0200
@@ -23,7 +23,8 @@
       'src/wizard.js',
       'src/board.js',
       'src/bus.js',
-      'src/plugins.js'
+      'src/plugins.js',
+      'src/html_sanitizer.js'
   ];
 
   // Project configuration.
diff -r 6d7c2dbd02a4 -r bea22c5b2072 src/html_sanitizer.js
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/src/html_sanitizer.js     Mon Jun 29 17:33:06 2020 +0200
@@ -0,0 +1,105 @@
+/*
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+(function () {
+    'use strict';
+
+    var tag_whitelist = {
+        B: true,
+        BODY: true,
+        BR: true,
+        DIV: true,
+        FONT: true,
+        I: true,
+        U: true,
+    };
+
+    var attribute_whitelist = {
+        align: true,
+        color: true,
+        face: true,
+        size: true,
+    };
+
+    Sao.HtmlSanitizer = {};
+    Sao.HtmlSanitizer.sanitize = function(input) {
+        input = input.trim();
+        // to save performance and not create iframe
+        if (input == "") return "";
+
+        // firefox "bogus node" workaround
+        if (input == "<br>") return "";
+
+        var iframe = document.createElement('iframe');
+        if (iframe.sandbox === undefined) {
+            // Browser does not support sandboxed iframes
+            console.warn("Your browser do not support sandboxed iframes," +
+                " unable to sanitize HTML.");
+            return input;
+        }
+        iframe.sandbox = 'allow-same-origin';
+        iframe.style.display = 'none';
+        // necessary so the iframe contains a document
+        document.body.appendChild(iframe);
+        var iframedoc = (iframe.contentDocument ||
+            iframe.contentWindow.document);
+        // null in IE
+        if (iframedoc.body == null) {
+            iframedoc.write("<body></body>");
+        }
+        iframedoc.body.innerHTML = input;
+
+        function make_sanitized_copy(node) {
+            var new_node;
+            if (node.nodeType == Node.TEXT_NODE) {
+                new_node = node.cloneNode(true);
+            } else if (node.nodeType == Node.ELEMENT_NODE &&
+                    tag_whitelist[node.tagName]) {
+                //remove useless empty tags
+                if ((node.tagName != "BR") && node.innerHTML.trim() == "") {
+                    return document.createDocumentFragment();
+                }
+
+                new_node = iframedoc.createElement(node.tagName);
+
+                for (var i = 0; i < node.attributes.length; i++) {
+                    var attr = node.attributes[i];
+                    if (attribute_whitelist[attr.name]) {
+                        new_node.setAttribute(attr.name, attr.value);
+                    }
+                }
+                for (i = 0; i < node.childNodes.length; i++) {
+                    var sub_copy = make_sanitized_copy(node.childNodes[i]);
+                    new_node.appendChild(sub_copy, false);
+                }
+            } else {
+                new_node = document.createDocumentFragment();
+            }
+            return new_node;
+        }
+
+        var result_element = make_sanitized_copy(iframedoc.body);
+        document.body.removeChild(iframe);
+        // replace is just for cleaner code
+        return result_element.innerHTML
+            .replace(/<br[^>]*>(\S)/g, "<br>\n$1")
+            .replace(/div><div/g, "div>\n<div");
+    };
+})();
diff -r 6d7c2dbd02a4 -r bea22c5b2072 src/view/form.js
--- a/src/view/form.js  Mon Jun 29 17:29:45 2020 +0200
+++ b/src/view/form.js  Mon Jun 29 17:33:06 2020 +0200
@@ -2261,7 +2261,7 @@
                     this.input.attr('spellcheck', 'true');
                 }
             }
-            this.input.html(value);
+            this.input.html(Sao.HtmlSanitizer.sanitize(value || ''));
         },
         focus: function() {
             this.input.focus();
@@ -2279,7 +2279,8 @@
             this.field.set_client(this.record, value);
         },
         _normalize_markup: function(content) {
-            var el = jQuery('<div/>').html(content || '');
+            var el = jQuery('<div/>').html(
+                Sao.HtmlSanitizer.sanitize(content || ''));
             this._normalize(el);
             return el.html();
         },
diff -r 6d7c2dbd02a4 -r bea22c5b2072 tests/sao.js
--- a/tests/sao.js      Mon Jun 29 17:29:45 2020 +0200
+++ b/tests/sao.js      Mon Jun 29 17:33:06 2020 +0200
@@ -2800,6 +2800,25 @@
             ["Active: False", "Active: True"]));
     });
 
+    QUnit.test('HTML Sanitization', function() {
+        var examples = [
+            ["Test", "Test"],
+            ["<b>Test</b>", "<b>Test</b>"],
+            ["<div><b>Test</b></div>", "<div><b>Test</b></div>"],
+            ["<script>window.alert('insecure')</script>", ""],
+            ["<b><script>window.alert('insecure')</script>Test</b>",
+                "<b>Test</b>"],
+            ['<div align="left">Test</div>', '<div align="left">Test</div>'],
+            ['<font href="test" size="1">Test</font>',
+                '<font size="1">Test</font>'],
+        ];
+        for (var i = 0; i < examples.length; i++) {
+            var input = examples[i][0], output = examples[i][1];
+            QUnit.strictEqual(Sao.HtmlSanitizer.sanitize(input), output,
+                'Sao.HtmlSanitizer.sanitize(' + input + ')');
+        }
+    });
+
         /*
     QUnit.test('CRUD', function() {
         var run_tests = function() {

Reply via email to