CVE-2017-14920: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Stored XSS vulnerability in eGroupware Community Edition before 16.1.20170922 allows an unauthenticated remote attacker to inject JavaScript via the User-Agent HTTP header, which is mishandled during rendering by the application administrator.
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: 6.1
Published: 2017-09-30T01:29Z
Repository: https://github.com/EGroupware/egroupware
Language: JavaScript
Files changed: api/js/etemplate/et2_core_baseWidget.js, api/js/jsapi/egw_tooltip.js, calendar/js/et2_widget_event.js, calendar/templates/default/conflicts.xet
Commit date: 2017-09-05 19:04:45 +0200
Commit message: Set statustext value as text by default
- Introduce new boolean attribute called statustext_html in case we want
explicitly statustext gets rendered as html
- Fix Calendar tooltip with new statustext_html attribute
Diff: diff --git a/api/js/etemplate/et2_core_baseWidget.js b/api/js/etemplate/et2_core_baseWidget.js
index b384c2967..6acb3c928 100644
--- a/api/js/etemplate/et2_core_baseWidget.js
+++ b/api/js/etemplate/et2_core_baseWidget.js
@@ -30,10 +30,16 @@ var et2_baseWidget = (function(){ "use strict"; return et2_DOMWidget.extend(et2_
 			"name": "Tooltip",
 			"type": "string",
 			"description": "Tooltip which is shown for this element",
 			"translate": true
 		},
+		"statustext_html": {
+			"name": "Tooltip is html",
+			"type": "boolean",
+			"description": "Flag to allow html content in tooltip",
+			"default": false
+		},
 		"align": {
 			"name": "Align",
 			"type": "string",
 			"default": "left",
 			"description": "Position of this element in the parent hbox"
@@ -275,11 +281,11 @@ var et2_baseWidget = (function(){ "use strict"; return et2_DOMWidget.extend(et2_
 				this._tooltipElem = null;
 			}
 
 			if (_value && _value != '')
 			{
-				this.egw().tooltipBind(elem, _value);
+				this.egw().tooltipBind(elem, _value, this.options.statustext_html);
 				this._tooltipElem = elem;
 			}
 		}
 	},
 
diff --git a/api/js/jsapi/egw_tooltip.js b/api/js/jsapi/egw_tooltip.js
index c7a6b1144..64a76365d 100644
--- a/api/js/jsapi/egw_tooltip.js
+++ b/api/js/jsapi/egw_tooltip.js
@@ -104,20 +104,28 @@ egw.extend('tooltip', egw.MODULE_WND_LOCAL, function(_app, _wnd)
 
 	/**
 	 * Creates the tooltip_div with the given text.
 	 *
 	 * @param {string} _html
+	 * @param {boolean} _isHtml if set to true content gets appended as html
 	 */
-	function prepare(_html)
+	function prepare(_html, _isHtml)
 	{
 		// Free and null the old tooltip_div
 		hide();
 
 		//Generate the tooltip div, set it's text and append it to the body tag
 		tooltip_div = jQuery(_wnd.document.createElement('div'));
 		tooltip_div.hide();
-		tooltip_div.append(_html);
+		if (_isHtml)
+		{
+			tooltip_div.append(_html);
+		}
+		else
+		{
+			tooltip_div.text(_html)
+		}
 		tooltip_div.addClass("egw_tooltip");
 		jQuery(_wnd.document.body).append(tooltip_div);
 
 		//The tooltip should automatically hide when the mouse comes over it
 		tooltip_div.mouseenter(function() {
@@ -154,18 +162,18 @@ egw.extend('tooltip', egw.MODULE_WND_LOCAL, function(_app, _wnd)
 		 *
 		 * @param _elem is the element to which the tooltip should get bound. It
 		 * 	has to be a jQuery node.
 		 * @param _html is the html code which should be shown as tooltip.
 		 */
-		tooltipBind: function(_elem, _html) {
+		tooltipBind: function(_elem, _html, _isHtml) {
 			if (_html != '')
 			{
 				_elem.bind('mouseenter.tooltip', function(e) {
 					if (_elem != current_elem)
 					{
 						//Prepare the tooltip
-						prepare(_html);
+						prepare(_html, _isHtml);
 
 						// Set the current element the mouse is over and
 						// initialize the position variables
 						current_elem = _elem;
 						show_delta = 0;
diff --git a/calendar/js/et2_widget_event.js b/calendar/js/et2_widget_event.js
index 6583b7b39..7d144d8a9 100644
--- a/calendar/js/et2_widget_event.js
+++ b/calendar/js/et2_widget_event.js
@@ -77,10 +77,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten
 					event._copy_parent_actions();
 				}
 				// Tooltip
 				if(!event._tooltipElem)
 				{
+					event.options.statustext_html = true;
 					event.set_statustext(event._tooltip());
 					return event.div.trigger('mouseenter');
 				}
 				// Hacky to remove egw's tooltip border and let the mouse in
 				window.setTimeout(function() {
@@ -207,11 +208,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten
 			this._parent.removeChild(this);
 			if(event === null && parent && parent._out_of_view)
 			{
 				parent._out_of_view();
 			}
-			
+
 			// This should now cease to exist, as new events have been created
 			this.free();
 			return;
 		}
 
@@ -519,11 +520,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten
 			'<div class="calendar_calEventBody">'+
 				'<p style="margin: 0px;">'+
 				'<span class="calendar_calEventTitle">'+egw.htmlspecialchars(this.options.value.title)+'</span><br>'+
 				egw.htmlspecialchars(this.options.value.description)+'</p>'+
 				'<p style="margin: 2px 0px;">'+times+'</p>'+
-				(this.options.value.location ? '<p><span class="calendar_calEventLabel">'+this.egw().lang('Location') + '</span>:' + 
+				(this.options.value.location ? '<p><span class="calendar_calEventLabel">'+this.egw().lang('Location') + '</span>:' +
 				egw.htmlspecialchars(this.options.value.location)+'</p>' : '')+
 				(cat_label ? '<p><span class="calendar_calEventLabel">'+this.egw().lang('Category') + '</span>:' + cat_label +'</p>' : '')+
 				'<p><span class="calendar_calEventLabel">'+this.egw().lang('Participants')+'</span>:<br />'+
 					participants + '</p>'+ this._participant_summary(this.options.value.participants) +
 			'</div>'+
@@ -539,11 +540,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten
 	{
 		if( Object.keys(this.options.value.participants).length < 2)
 		{
 			return '';
 		}
-		
+
 		var participant_status = {A: 0, R: 0, T: 0, U: 0, D: 0};
 		var status_label = {A: 'accepted', R: 'rejected', T: 'tentative', U: 'unknown', D: 'delegated'};
 		var participant_summary = Object.keys(this.options.value.participants).length + ' ' + this.egw().lang('Participants')+': ';
 		var status_totals = [];
 
@@ -912,11 +913,11 @@ var et2_calendar_event = (function(){ "use strict"; return et2_valueWidget.exten
 			} catch (e) {
 				// something went wrong, but keep quiet about it
 			}
 		}
 	},
-	
+
 	/**
 	 * Link the actions to the DOM nodes / widget bits.
 	 *
 	 * @param {object} actions {ID: {attributes..}+} map of egw action information
 	 */
diff --git a/calendar/templates/default/conflicts.xet b/calendar/templates/default/conflicts.xet
index b672f0804..15dc3c39e 100644
--- a/calendar/templates/default/conflicts.xet
+++ b/calendar/templates/default/conflicts.xet
@@ -12,14 +12,14 @@
 					<column/>
 					<column/>
 				</columns>
 				<rows>
 					<row valign="top">
-						<image src="conflicts[$row][icon_participants]" statustext="@conflicts[$row][tooltip_participants]" no_lang="1"/>
+						<image src="conflicts[$row][icon_participants]" statustext_html="true" statustext="@conflicts[$row][tooltip_participants]" no_lang="1"/>
 						<image src="conflicts[$row][icon_recur]" label="@conflicts[$row][text_recur]" no_lang="1"/>
 						<description id="conflicts[$row][time]" no_lang="1"/>
-						<vbox statustext="conflict[$row][tooltip]" orient=",0,0">
+						<vbox statustext="conflict[$row][tooltip]" statustext_html="true" orient=",0,0">
 							<description id="conflicts[$row][title]" no_lang="1" options="b"/>
 							<description id="conflicts[$row][conflicting_participants]" no_lang="1"/>
 						</vbox>
 					</row>
 				</rows>

Vulnerable code: 				this.egw().tooltipBind(elem, _value);
	function prepare(_html)
		tooltip_div.append(_html);
		tooltipBind: function(_elem, _html) {
						prepare(_html);
			
				(this.options.value.location ? '<p><span class="calendar_calEventLabel">'+this.egw().lang('Location') + '</span>:' + 
		
	
						<image src="conflicts[$row][icon_participants]" statustext="@conflicts[$row][tooltip_participants]" no_lang="1"/>
						<vbox statustext="conflict[$row][tooltip]" orient=",0,0">
Fixed code: 		"statustext_html": {
			"name": "Tooltip is html",
			"type": "boolean",
			"description": "Flag to allow html content in tooltip",
			"default": false
		},
				this.egw().tooltipBind(elem, _value, this.options.statustext_html);
	 * @param {boolean} _isHtml if set to true content gets appended as html
	function prepare(_html, _isHtml)
		if (_isHtml)
		{
			tooltip_div.append(_html);
		}
		else
		{
			tooltip_div.text(_html)
		}
		tooltipBind: function(_elem, _html, _isHtml) {
						prepare(_html, _isHtml);
					event.options.statustext_html = true;

				(this.options.value.location ? '<p><span class="calendar_calEventLabel">'+this.egw().lang('Location') + '</span>:' +


						<image src="conflicts[$row][icon_participants]" statustext_html="true" statustext="@conflicts[$row][tooltip_participants]" no_lang="1"/>
						<vbox statustext="conflict[$row][tooltip]" statustext_html="true" orient=",0,0">