CVE-2017-14922: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Stored XSS vulnerability via IMG element at "History" of Profile, Calendar, Tasks, and CRM in Tine 2.0 Community Edition before 2017.08.4 allows an authenticated user to inject JavaScript, which is mishandled during rendering by the application administrator and other users.
CWE description: 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.
CVSS v3 base score: 5.4
Published: 2017-09-30T01:29Z
Repository: https://github.com/tine20/Tine-2.0-Open-Source-Groupware-and-CRM
Language: JavaScript
Files changed: tine20/Addressbook/js/ContactGrid.js, tine20/Felamimail/js/FolderSelectPanel.js, tine20/Felamimail/js/TreePanel.js, tine20/Filemanager/js/NodeTreePanel.js, tine20/Phone/js/PhoneTreePanel.js, tine20/Tinebase/js/ux/Percentage.js, tine20/Tinebase/js/widgets/ActivitiesPanel.js, tine20/Tinebase/js/widgets/container/TreePanel.js, tine20/Tinebase/js/widgets/grid/FilterPanel.js, tine20/Tinebase/js/widgets/grid/FilterStructureTreePanel.js, tine20/Tinebase/js/widgets/persistentfilter/PickerPanel.js, tine20/Tinebase/js/widgets/relation/GridRenderer.js, tine20/Tinebase/js/widgets/tree/ContextMenu.js, tine20/Tinebase/js/widgets/tree/Loader.js
Commit date: 2017-09-08 17:41:08 +0200
Commit message: name might not be displayed correctly

Change-Id: I949c7a80b5356f393d269d3004bd6800772ac63b
Reviewed-on: http://gerrit.tine20.com/customers/5618
Reviewed-by: Cornelius Weiss <c.weiss@metaways.de>
Tested-by: Cornelius Weiss <c.weiss@metaways.de>

Diff: diff --git a/tine20/Addressbook/js/ContactGrid.js b/tine20/Addressbook/js/ContactGrid.js
index 576097d3dd..257a24485e 100644
--- a/tine20/Addressbook/js/ContactGrid.js
+++ b/tine20/Addressbook/js/ContactGrid.js
@@ -257,16 +257,16 @@ Tine.Addressbook.ContactGridPanel.contactTypeRenderer = function(data, cell, rec
     return '<div ext:qtip="' + qtipText + '" style="background-position:0px;" class="' + cssClass + '">&#160</div>';
 };
 
 Tine.Addressbook.ContactGridPanel.displayNameRenderer = function(data) {
     var i18n = Tine.Tinebase.appMgr.get('Addressbook').i18n;
-    return data ? data : ('<div class="renderer_displayNameRenderer_noName">' + i18n._('No name') + '</div>');
+    return data ? Ext.util.Format.htmlEncode(data) : ('<div class="renderer_displayNameRenderer_noName">' + i18n._('No name') + '</div>');
 };
 
 Tine.Addressbook.ContactGridPanel.countryRenderer = function(data) {
     data = Locale.getTranslationData('CountryList', data);
-    return data;
+    return Ext.util.Format.htmlEncode(data);
 };
 
 /**
  * Statically constructs the columns used to represent a contact. Reused by ListMemberGridPanel
  *
diff --git a/tine20/Felamimail/js/FolderSelectPanel.js b/tine20/Felamimail/js/FolderSelectPanel.js
index fdfc8f88c0..79186c42b9 100755
--- a/tine20/Felamimail/js/FolderSelectPanel.js
+++ b/tine20/Felamimail/js/FolderSelectPanel.js
@@ -121,11 +121,11 @@ Tine.Felamimail.FolderSelectPanel = Ext.extend(Ext.Panel, {
                     globalname: '',
                     draggable: false,
                     allowDrop: false,
                     expanded: false,
                     text: Ext.util.Format.htmlEncode(record.get('name')),
-                    qtip: Tine.Tinebase.common.doubleEncode(record.get('host')),
+                    qtip: Ext.util.Format.htmlEncode(record.get('host')),
                     leaf: false,
                     cls: 'felamimail-node-account',
                     delimiter: record.get('delimiter'),
                     ns_personal: record.get('ns_personal'),
                     account_id: record.data.id
diff --git a/tine20/Felamimail/js/TreePanel.js b/tine20/Felamimail/js/TreePanel.js
index 86a0089e53..7260c9d312 100644
--- a/tine20/Felamimail/js/TreePanel.js
+++ b/tine20/Felamimail/js/TreePanel.js
@@ -779,11 +779,11 @@ Ext.extend(Tine.Felamimail.TreePanel, Ext.tree.TreePanel, {
             globalname: '',
             draggable: false,
             allowDrop: false,
             expanded: false,
             text: Ext.util.Format.htmlEncode(record.get('name')),
-            qtip: Tine.Tinebase.common.doubleEncode(record.get('host')),
+            qtip: Ext.util.Format.htmlEncode(record.get('host')),
             leaf: false,
             cls: 'felamimail-node-account',
             delimiter: record.get('delimiter'),
             ns_personal: record.get('ns_personal'),
             account_id: record.data.id,
diff --git a/tine20/Filemanager/js/NodeTreePanel.js b/tine20/Filemanager/js/NodeTreePanel.js
index 99c2b1c029..fdbda80bf5 100644
--- a/tine20/Filemanager/js/NodeTreePanel.js
+++ b/tine20/Filemanager/js/NodeTreePanel.js
@@ -251,14 +251,14 @@ Ext.extend(Tine.Filemanager.NodeTreePanel, Tine.widgets.container.TreePanel, {
         attr.leaf = false;
         
         if(attr.name && typeof attr.name == 'object') {
             Ext.apply(attr, {
                 text: Ext.util.Format.htmlEncode(attr.name.name),
-                qtip: Tine.Tinebase.common.doubleEncode(attr.name.name)
+                qtip: Ext.util.Format.htmlEncode(attr.name.name)
             });
         }
-        
+
         // copy 'real' data to a node record NOTE: not a full record as we have no record reader here
         var nodeData = Ext.copyTo({}, attr, Tine.Filemanager.Model.Node.getFieldNames());
         attr.nodeRecord = new Tine.Filemanager.Model.Node(nodeData);
     },
     
@@ -575,17 +575,17 @@ Ext.extend(Tine.Filemanager.NodeTreePanel, Tine.widgets.container.TreePanel, {
         }
         
         var newNodeRecord = new Tine.Filemanager.Model.Node(nodeData);
         
         var newNode = new Ext.tree.AsyncTreeNode({
-            text: nodeName,
+            text: Ext.util.Format.htmlEncode(nodeName),
             path: nodeData.path,
             name: nodeData.name,
             nodeRecord: newNodeRecord,
             account_grants: nodeData.account_grants,
             id: nodeData.id
-        })
+        });
         
         newNode.attributes.nodeRecord.beginEdit();
         newNode.attributes.nodeRecord.set('path', nodeData.path);
         newNode.attributes.nodeRecord.endEdit();
         
diff --git a/tine20/Phone/js/PhoneTreePanel.js b/tine20/Phone/js/PhoneTreePanel.js
index 915f32aa5c..c1ab2df497 100644
--- a/tine20/Phone/js/PhoneTreePanel.js
+++ b/tine20/Phone/js/PhoneTreePanel.js
@@ -99,17 +99,17 @@ Tine.Phone.PhoneTreePanel = Ext.extend(Ext.tree.TreePanel, {
 
         // add phones from store to tree menu
         this.store.each(function(record) {
             var label = (record.data.description == '') 
                ? record.data.macaddress 
-               : Ext.util.Format.ellipsis(record.data.description, 30);
+               : Ext.util.Format.ellipsis(Ext.util.Format.htmlEncode(record.data.description), 30);
             var node = new Ext.tree.TreeNode({
                 id: record.id,
                 record: record,
                 text: label,
                 iconCls: 'PhoneIconCls',
-                qtip: Tine.Tinebase.common.doubleEncode(record.data.description),
+                qtip: Ext.util.Format.htmlEncode(record.data.description),
                 leaf: true
             });
             rootNode.appendChild(node);
         }, this);
     },
diff --git a/tine20/Tinebase/js/ux/Percentage.js b/tine20/Tinebase/js/ux/Percentage.js
index 8b8a1f2ea1..ffe412d64b 100644
--- a/tine20/Tinebase/js/ux/Percentage.js
+++ b/tine20/Tinebase/js/ux/Percentage.js
@@ -190,11 +190,11 @@ Ext.ux.PercentRendererWithName = function(value, metadata, record) {
     
         if(record.get('status') == 'uploading') {
             metadata.css = 'x-tinebase-uploadrow';
         }
         
-        return fileName;
+        return Ext.util.Format.htmlEncode(fileName);
     }
     
     if (! Ext.ux.PercentRendererWithName.template) {
         Ext.ux.PercentRendererWithName.template = new Ext.XTemplate(
             '<div class="x-progress-wrap PercentRenderer" style="{display}">',
@@ -219,19 +219,19 @@ Ext.ux.PercentRendererWithName = function(value, metadata, record) {
     var fileName = value;
 
     if (typeof value == 'object') {
         fileName = value.name;
     }
-    
+    fileName = Ext.util.Format.htmlEncode(fileName);
     var percent = record.get('progress');
 
     var additionalStyle = '';
     if(record.get('status') == 'paused' && percent < 100) {
         fileName = _('(paused)') + '&#160;&#160;' + fileName;
         additionalStyle = 'background-image: url(\'styles/images/tine20/progress/progress-bg-y.gif\') !important;';
     }
-       
+
     var display = 'width:0px';
     if(percent > -1 && percent < 100) {
         display = '';
         var renderedField = Ext.ux.PercentRendererWithName.template.apply({percent: percent, display: display, fileName: fileName
             , additionalStyle: additionalStyle}) ;
diff --git a/tine20/Tinebase/js/widgets/ActivitiesPanel.js b/tine20/Tinebase/js/widgets/ActivitiesPanel.js
index a71ba8df8a..c6efc1d85f 100644
--- a/tine20/Tinebase/js/widgets/ActivitiesPanel.js
+++ b/tine20/Tinebase/js/widgets/ActivitiesPanel.js
@@ -120,10 +120,12 @@ Tine.widgets.activities.ActivitiesTabPanel = Ext.extend(Ext.Panel, {
 
     noteRenderer: function(note) {
         var recordClass = Tine.Tinebase.data.RecordMgr.get(this.record_model),
             app = Tine.Tinebase.appMgr.get(this.app);
 
+        note = Ext.util.Format.htmlEncode(note);
+        
         if (recordClass) {
             Ext.each(recordClass.getFieldDefinitions(), function(field) {
                 if (field.label) {
                     note = String(note).replace(field.name, '<br>' + app.i18n._hidden(field.label));
                     //map[field.name] = '<br>' + this.app.i18n._hidden(field.label);
diff --git a/tine20/Tinebase/js/widgets/container/TreePanel.js b/tine20/Tinebase/js/widgets/container/TreePanel.js
index 510693fca2..5e5fb5c251 100644
--- a/tine20/Tinebase/js/widgets/container/TreePanel.js
+++ b/tine20/Tinebase/js/widgets/container/TreePanel.js
@@ -493,16 +493,19 @@ Ext.extend(Tine.widgets.container.TreePanel, Ext.tree.TreePanel, {
         if (! attr.name && attr.path) {
             attr.name = Tine.Tinebase.container.path2name(attr.path, this.containerName, this.containersName);
         }
         
         Ext.applyIf(attr, {
-            text: Ext.util.Format.htmlEncode(attr.name),
-            qtip: Tine.Tinebase.common.doubleEncode(attr.name),
+            text: attr.name,
+            qtip: attr.name,
             leaf: !!attr.account_grants,
             allowDrop: !!attr.account_grants && attr.account_grants.addGrant
         });
-        
+
+        attr.text = Ext.util.Format.htmlEncode(attr.text);
+        attr.qtip = Ext.util.Format.htmlEncode(attr.qtip);
+
         // copy 'real' data to container space
         attr.container = Ext.copyTo({}, attr, Tine.Tinebase.Model.Container.getFieldNames());
     },
     
     /**
diff --git a/tine20/Tinebase/js/widgets/grid/FilterPanel.js b/tine20/Tinebase/js/widgets/grid/FilterPanel.js
index a9b08c86b8..f79b42a3fd 100644
--- a/tine20/Tinebase/js/widgets/grid/FilterPanel.js
+++ b/tine20/Tinebase/js/widgets/grid/FilterPanel.js
@@ -518,11 +518,11 @@ Ext.extend(Tine.widgets.grid.FilterPanel, Ext.Panel, {
             return filters;
         }
         
         for (var id in this.filterPanels) {
             if (this.filterPanels.hasOwnProperty(id) && this.filterPanels[id].isActive) {
-                filters.push({'condition': 'AND', 'filters': this.filterPanels[id].getValue(), 'id': id, label: this.filterPanels[id].title});
+                filters.push({'condition': 'AND', 'filters': this.filterPanels[id].getValue(), 'id': id, label: Ext.util.Format.htmlDecode(this.filterPanels[id].title)});
             }
         }
         
         // NOTE: always trigger a OR condition, otherwise we sould loose inactive FilterPanles
         //return filters.length == 1 ? filters[0].filters : [{'condition': 'OR', 'filters': filters}];
diff --git a/tine20/Tinebase/js/widgets/grid/FilterStructureTreePanel.js b/tine20/Tinebase/js/widgets/grid/FilterStructureTreePanel.js
index 7dbc3eaec5..298b81b49a 100644
--- a/tine20/Tinebase/js/widgets/grid/FilterStructureTreePanel.js
+++ b/tine20/Tinebase/js/widgets/grid/FilterStructureTreePanel.js
@@ -80,10 +80,13 @@ Tine.widgets.grid.FilterStructureTreePanel = Ext.extend(Ext.tree.TreePanel, {
         this.filterPanel.on('filterpanelremoved', this.onFilterPanelRemoved, this);
         this.filterPanel.on('filterpanelactivate', this.onFilterPanelActivate, this);
         this.filterPanel.activeFilterPanel.on('titlechange', this.onFilterPanelTitleChange, this);
         
         this.editor = new Ext.tree.TreeEditor(this);
+        this.editor.on('startedit', function(el, value) {
+            this.editor.setValue(Ext.util.Format.htmlDecode(value));
+        }, this);
         
         Tine.widgets.grid.FilterStructureTreePanel.superclass.initComponent.call(this);
     },
     
     onContextMenu: function(node, e) {
@@ -111,11 +114,11 @@ Tine.widgets.grid.FilterStructureTreePanel = Ext.extend(Ext.tree.TreePanel, {
      * @param {String} text
      * @param {String} oldText
      */
     onNodeTextChange: function(node, text, oldText) {
         if (node.attributes && node.attributes.filterPanel) {
-            node.attributes.filterPanel.setTitle(text);
+            node.attributes.filterPanel.setTitle(Ext.util.Format.htmlEncode(text));
         }
     },
     
     /**
      * called when a node got inserted to this tree
diff --git a/tine20/Tinebase/js/widgets/persistentfilter/PickerPanel.js b/tine20/Tinebase/js/widgets/persistentfilter/PickerPanel.js
index 0d8077e626..5f346e8e8e 100644
--- a/tine20/Tinebase/js/widgets/persistentfilter/PickerPanel.js
+++ b/tine20/Tinebase/js/widgets/persistentfilter/PickerPanel.js
@@ -624,11 +624,11 @@ Tine.widgets.persistentfilter.PickerTreePanelLoader = Ext.extend(Tine.widgets.tr
 
                 if (isPersistentFilter) {
                     Ext.apply(attr, {
                         isPersistentFilter : isPersistentFilter,
                         text : Ext.util.Format.htmlEncode(this.app.i18n._hidden(attr.name)),
-                        qtip : Tine.Tinebase.common.doubleEncode(attr.description ? this.app.i18n._hidden(attr.description) + ' ' + addText : addText),
+                        qtip : Ext.util.Format.htmlEncode(attr.description ? this.app.i18n._hidden(attr.description) + ' ' + addText : addText),
                         selected : attr.id === this.selectedFilterId,
                         id : attr.id,
 
                         sorting : attr.sorting,
                         draggable : true,
diff --git a/tine20/Tinebase/js/widgets/relation/GridRenderer.js b/tine20/Tinebase/js/widgets/relation/GridRenderer.js
index 55c02d874b..58610de266 100644
--- a/tine20/Tinebase/js/widgets/relation/GridRenderer.js
+++ b/tine20/Tinebase/js/widgets/relation/GridRenderer.js
@@ -58,10 +58,10 @@ Ext.extend(Tine.widgets.relation.GridRenderer, Ext.Component, {
         
         for (var index = 0; index < relations.length; index++) {
             var el = relations[index];
             if (el.type == this.type && el.related_model == this.relModel) {
                 var record = new this.recordClass(el.related_record);
-                return record.getTitle();
+                return Ext.util.Format.htmlEncode(record.getTitle());
             }
         }
     }
 });
diff --git a/tine20/Tinebase/js/widgets/tree/ContextMenu.js b/tine20/Tinebase/js/widgets/tree/ContextMenu.js
index 76507b4398..e3fdb5edf3 100644
--- a/tine20/Tinebase/js/widgets/tree/ContextMenu.js
+++ b/tine20/Tinebase/js/widgets/tree/ContextMenu.js
@@ -280,11 +280,11 @@ Tine.widgets.tree.ContextMenu = {
             var node = this.scope.ctxNode;
             Ext.MessageBox.show({
                 title: String.format(_('Rename {0}'), this.nodeName),
                 msg: String.format(_('Please enter the new name of the {0}:'), this.nodeName),
                 buttons: Ext.MessageBox.OKCANCEL,
-                value: node.attributes.longName || node.text,
+                value: Ext.util.Format.htmlDecode(node.attributes.longName || node.text),
                 fn: function(_btn, _text){
                     if (_btn == 'ok') {
                         if (! _text) {
                             Ext.Msg.alert(String.format(_('Not renamed {0}'), this.nodeName), String.format(_('You have to supply a {0} name!'), this.nodeName));
                             return;
@@ -324,11 +324,11 @@ Tine.widgets.tree.ContextMenu = {
                             params: params,
                             scope: this,
                             success: function(_result, _request){
                             
                                 var nodeData = Ext.util.JSON.decode(_result.responseText);
-                                node.setText(_text);
+                                node.setText(Ext.util.Format.htmlEncode(_text));
                                 
                                 this.scope.fireEvent('containerrename', nodeData, node, _text);
                                                               
                             },
                             failure: function(result, request) {
diff --git a/tine20/Tinebase/js/widgets/tree/Loader.js b/tine20/Tinebase/js/widgets/tree/Loader.js
index 300e461577..8b117a4406 100644
--- a/tine20/Tinebase/js/widgets/tree/Loader.js
+++ b/tine20/Tinebase/js/widgets/tree/Loader.js
@@ -118,11 +118,11 @@ Tine.widgets.tree.Loader = Ext.extend(Ext.tree.TreeLoader, {
 
                 parentNode = child.children;
             }, this);
 
             node.longName = node.name;
-            node.text = node.name = Ext.util.Format.htmlEncode(containerName);
+            node.text = node.name = containerName;
 
             parentNode.push(node);
         }, this);
 
         response.responseData = newResponse;

Vulnerable code:     return data ? data : ('<div class="renderer_displayNameRenderer_noName">' + i18n._('No name') + '</div>');
    return data;
                    qtip: Tine.Tinebase.common.doubleEncode(record.get('host')),
            qtip: Tine.Tinebase.common.doubleEncode(record.get('host')),
                qtip: Tine.Tinebase.common.doubleEncode(attr.name.name)
        
            text: nodeName,
        })
               : Ext.util.Format.ellipsis(record.data.description, 30);
                qtip: Tine.Tinebase.common.doubleEncode(record.data.description),
        return fileName;
    
       
            text: Ext.util.Format.htmlEncode(attr.name),
            qtip: Tine.Tinebase.common.doubleEncode(attr.name),
        
                filters.push({'condition': 'AND', 'filters': this.filterPanels[id].getValue(), 'id': id, label: this.filterPanels[id].title});
            node.attributes.filterPanel.setTitle(text);
                        qtip : Tine.Tinebase.common.doubleEncode(attr.description ? this.app.i18n._hidden(attr.description) + ' ' + addText : addText),
                return record.getTitle();
                value: node.attributes.longName || node.text,
                                node.setText(_text);
            node.text = node.name = Ext.util.Format.htmlEncode(containerName);
Fixed code:     return data ? Ext.util.Format.htmlEncode(data) : ('<div class="renderer_displayNameRenderer_noName">' + i18n._('No name') + '</div>');
    return Ext.util.Format.htmlEncode(data);
                    qtip: Ext.util.Format.htmlEncode(record.get('host')),
            qtip: Ext.util.Format.htmlEncode(record.get('host')),
                qtip: Ext.util.Format.htmlEncode(attr.name.name)

            text: Ext.util.Format.htmlEncode(nodeName),
        });
               : Ext.util.Format.ellipsis(Ext.util.Format.htmlEncode(record.data.description), 30);
                qtip: Ext.util.Format.htmlEncode(record.data.description),
        return Ext.util.Format.htmlEncode(fileName);
    fileName = Ext.util.Format.htmlEncode(fileName);

        note = Ext.util.Format.htmlEncode(note);
        
            text: attr.name,
            qtip: attr.name,

        attr.text = Ext.util.Format.htmlEncode(attr.text);
        attr.qtip = Ext.util.Format.htmlEncode(attr.qtip);

                filters.push({'condition': 'AND', 'filters': this.filterPanels[id].getValue(), 'id': id, label: Ext.util.Format.htmlDecode(this.filterPanels[id].title)});
        this.editor.on('startedit', function(el, value) {
            this.editor.setValue(Ext.util.Format.htmlDecode(value));
        }, this);
            node.attributes.filterPanel.setTitle(Ext.util.Format.htmlEncode(text));
                        qtip : Ext.util.Format.htmlEncode(attr.description ? this.app.i18n._hidden(attr.description) + ' ' + addText : addText),
                return Ext.util.Format.htmlEncode(record.getTitle());
                value: Ext.util.Format.htmlDecode(node.attributes.longName || node.text),
                                node.setText(Ext.util.Format.htmlEncode(_text));
            node.text = node.name = containerName;