CVE-2019-14548 (JavaScript, published 2019-08-05T19:15Z)
Description: value: An issue was discovered in EspoCRM before 5.6.9. Stored XSS in the body of an Article was executed when a victim opens articles received through mail. This Article can be formed by an attacker using the Knowledge Base feature in the tab list. The attacker could inject malicious JavaScript inside the body of the article, thus helping him steal victims' cookies (hence compromising their accounts).
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/espocrm/espocrm
Files changed:
client/res/templates/attachment/fields/name/detail.tpl
client/src/view-helper.js
client/src/views/attachment/fields/name.js
client/src/views/fields/wysiwyg.js
client/src/views/preferences/fields/dashboard-tab-list.js
Commit message: xss fixes


Vulnerable code:
<span class="fas fa-paperclip small"></span> <a href="{{url}}">{{{value}}}</a>
Espo.define('views/attachment/fields/name', 'views/fields/varchar', function (Dep) {
            value = value || '';
            value = value.replace(/<[\/]{0,1}(base)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(object)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(embed)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(applet)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(iframe)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(script)[^><]*>/gi, '');
            value = value.replace(/<[^><]*([^a-z]{1}on[a-z]+)=[^><]*>/gi, function (match) {
                return match.replace(/[^a-z]{1}on[a-z]+=/gi, ' data-handler-stripped=');
            });

            value = value.replace(/href=" *javascript\:(.*?)"/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/href=' *javascript\:(.*?)'/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/src=" *javascript\:(.*?)"/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/src=' *javascript\:(.*?)'/gi, function(m, $1) {
                return 'removed=""';
            });
            return value;
Espo.define('views/preferences/fields/dashboard-tab-list', 'views/fields/array', function (Dep) {

            var translatedValue = this.translatedOptions[value] || value;
            '<div class="list-group-item link-with-role form-inline" data-value="' + value + '">' +
                    '<input data-name="translatedValue" data-value="' + value + '" class="role form-control input-sm" value="'+translatedValue+'">' +
                    '<a href="javascript:" class="pull-right" data-value="' + value + '" data-action="removeValue"><span class="fas fa-times"></a>' +
                data.translatedOptions[value] = this.$el.find('input[data-name="translatedValue"][data-value="'+value+'"]').val() || value;

Fixed code:
<span class="fas fa-paperclip small"></span> <a href="{{url}}">{{value}}</a>

        moderateSanitizeHtml: function (value) {
            value = value || '';
            value = value.replace(/<[\/]{0,1}(base)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(object)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(embed)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(applet)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(iframe)[^><]*>/gi, '');
            value = value.replace(/<[\/]{0,1}(script)[^><]*>/gi, '');
            value = value.replace(/<[^><]*([^a-z]{1}on[a-z]+)=[^><]*>/gi, function (match) {
                return match.replace(/[^a-z]{1}on[a-z]+=/gi, ' data-handler-stripped=');
            });

            value = this.stripEventHandlersInHtml(value);

            value = value.replace(/href=" *javascript\:(.*?)"/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/href=' *javascript\:(.*?)'/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/src=" *javascript\:(.*?)"/gi, function(m, $1) {
                return 'removed=""';
            });
            value = value.replace(/src=' *javascript\:(.*?)'/gi, function(m, $1) {
                return 'removed=""';
            });

            return value;
        },

        stripEventHandlersInHtml: function (html) {
            function stripHTML(){
                html = html.slice(0, strip) + html.slice(j);
                j = strip;
                strip = false;
            }
            function isValidTagChar(str) {
                return str.match(/[a-z?\\\/!]/i);
            }
            var strip = false;
            var lastQuote = false;
            for (var i = 0; i<html.length; i++){
                if (html[i] === "<" && html[i+1] && isValidTagChar(html[i+1])) {
                    i++;
                    for (var j = i; j<html.length; j++){
                        if (!lastQuote && html[j] === ">"){
                            if (strip) {
                                stripHTML();
                            }
                            i = j;
                            break;
                        }
                        if (lastQuote === html[j]){
                            lastQuote = false;
                            continue;
                        }
                        if (!lastQuote && html[j-1] === "=" && (html[j] === "'" || html[j] === '"')){
                            lastQuote = html[j];
                        }
                        if (!lastQuote && html[j-2] === " " && html[j-1] === "o" && html[j] === "n"){
                            strip = j-2;
                        }
                        if (strip && html[j] === " " && !lastQuote){
                            stripHTML();
                        }
                    }
                }
            }
            return html;
        },
define('views/attachment/fields/name', 'views/fields/varchar', function (Dep) {
           return this.getHelper().moderateSanitizeHtml(value);
define('views/preferences/fields/dashboard-tab-list', 'views/fields/array', function (Dep) {
            value = value.toString();
            var valueSanitized = this.escapeValue(value);
            var translatedValue = this.escapeValue(this.translatedOptions[value] || value);
            '<div class="list-group-item link-with-role form-inline" data-value="' + valueSanitized + '">' +
                    '<input data-name="translatedValue" data-value="' + valueSanitized + '" class="role form-control input-sm" value="'+translatedValue+'">' +
                    '<a href="javascript:" class="pull-right" data-value="' + valueSanitized + '" data-action="removeValue"><span class="fas fa-times"></a>' +
                var valueInternal = value.replace(/"/g, '\\"');
                data.translatedOptions[value] = this.$el.find('input[data-name="translatedValue"][data-value="'+valueInternal+'"]').val() || value;