CVE-2019-14547 (JavaScript, published 2019-08-05T19:15Z)
Description: value: An issue was discovered in EspoCRM before 5.6.9. Stored XSS was executed when a attacker sends an attachment to admin with malicious JavaScript in the filename. This JavaScript executed when an admin selects the particular file from the list of all attachments. The attacker could inject the JavaScript inside the filename and send it to users, 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;