changeset 8d59d1bf1568 in sao:5.2 details: https://hg.tryton.org/sao?cmd=changeset;node=8d59d1bf1568 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 bb05591968e8 -r 8d59d1bf1568 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.2.17 - 2020-06-16 diff -r bb05591968e8 -r 8d59d1bf1568 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 bb05591968e8 -r 8d59d1bf1568 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 bb05591968e8 -r 8d59d1bf1568 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 bb05591968e8 -r 8d59d1bf1568 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 @@ -1965,7 +1965,7 @@ this.input.attr('spellcheck', 'true'); } } - this.input.html(value); + this.input.html(Sao.HtmlSanitizer.sanitize(value || '')); }, focus: function() { this.input.focus(); @@ -1983,7 +1983,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 bb05591968e8 -r 8d59d1bf1568 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 @@ -2498,6 +2498,25 @@ }); }); + 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() {