CVE-2020-15253: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Versions of Grocy <= 2.7.1 are vulnerable to Cross-Site Scripting via the Create Shopping List module, that is rendered upon deleting that Shopping List. The issue was also found in users, batteries, chores, equipment, locations, quantity units, shopping locations, tasks, taskcategories, product groups, recipes and products. Authentication is required to exploit these issues and Grocy should not be publicly exposed. The linked reference details a proof-of-concept.
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: 4.8
Published: 2020-10-14T19:15Z
Repository: https://github.com/grocy/grocy
Language: JavaScript
Files changed: public/js/extensions.js, public/viewjs/batteries.js, public/viewjs/chores.js, public/viewjs/components/productpicker.js, public/viewjs/equipment.js, public/viewjs/locations.js, public/viewjs/mealplan.js, public/viewjs/productgroups.js, public/viewjs/products.js, public/viewjs/quantityunits.js, public/viewjs/recipeform.js, public/viewjs/recipes.js, public/viewjs/shoppinglist.js, public/viewjs/shoppinglocations.js, public/viewjs/taskcategories.js, public/viewjs/tasks.js, public/viewjs/userentities.js, public/viewjs/userfields.js, public/viewjs/users.js
Commit date: 2020-09-08 18:10:30 +0200
Commit message: Excape HTML (where needed, for bootbox) (references #996)

Diff: diff --git a/public/js/extensions.js b/public/js/extensions.js
index e24b5f6..9ed9e1f 100644
--- a/public/js/extensions.js
+++ b/public/js/extensions.js
@@ -170,25 +170,33 @@ function animateCSS(selector, animationName, callback, speed = "faster")
 		}
 	}
 
 	nodes.on('animationend', handleAnimationEnd);
 }
+
 function RandomString()
 {
 	return Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
 }
+
 function getQRCodeForContent(url)
 {
 	var qr = qrcode(0, 'L');
 	qr.addData(url);
 	qr.make();
 	return qr.createImgTag(10, 5);
 }
+
 function getQRCodeForAPIKey(apikey_type, apikey_key)
 {
 	var content = U('/api') + '|' + apikey_key;
 	if (apikey_type === 'special-purpose-calendar-ical')
 	{
 		content = U('/api/calendar/ical?secret=' + apikey_key);
 	}
 	return getQRCodeForContent(content);
 }
+
+function SanitizeHtml(input)
+{
+	return $("<div/>").text(input).html();
+}
diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js
index 0829ebe..71e1a21 100644
--- a/public/viewjs/batteries.js
+++ b/public/viewjs/batteries.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	batteriesTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.battery-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-battery-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-battery-name'));
 	var objectId = $(e.currentTarget).attr('data-battery-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete battery "%s"?', objectName),
 		buttons: {
diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js
index 7d3ca85..a120bd6 100644
--- a/public/viewjs/chores.js
+++ b/public/viewjs/chores.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	choresTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.chore-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-chore-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-chore-name'));
 	var objectId = $(e.currentTarget).attr('data-chore-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete chore "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js
index 16d4bd5..f90623c 100644
--- a/public/viewjs/components/productpicker.js
+++ b/public/viewjs/components/productpicker.js
@@ -160,11 +160,11 @@ $('#product_id_text_input').on('blur', function(e)
 				addProductWorkflowsAdditionalCssClasses = "d-none";
 			}
 
 			Grocy.Components.ProductPicker.PopupOpen = true;
 			bootbox.dialog({
-				message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input),
+				message: __t('"%s" could not be resolved to a product, how do you want to proceed?', SanitizeHtml(input)),
 				title: __t('Create or assign product'),
 				onEscape: function()
 				{
 					Grocy.Components.ProductPicker.PopupOpen = false;
 					Grocy.Components.ProductPicker.SetValue('');
diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js
index 0bdb87e..ebfb003 100644
--- a/public/viewjs/equipment.js
+++ b/public/viewjs/equipment.js
@@ -66,11 +66,11 @@ $("#search").on("keyup", Delay(function()
 	equipmentTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.equipment-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-equipment-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-equipment-name'));
 	var objectId = $(e.currentTarget).attr('data-equipment-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete equipment "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/locations.js b/public/viewjs/locations.js
index 28947d3..6bab2dc 100644
--- a/public/viewjs/locations.js
+++ b/public/viewjs/locations.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	locationsTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.location-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-location-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-location-name'));
 	var objectId = $(e.currentTarget).attr('data-location-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete location "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js
index 002ffee..13c4c00 100644
--- a/public/viewjs/mealplan.js
+++ b/public/viewjs/mealplan.js
@@ -561,11 +561,11 @@ $(document).on('click', '.recipe-order-missing-button', function(e)
 {
 	// Remove the focus from the current button
 	// to prevent that the tooltip stays until clicked anywhere else
 	document.activeElement.blur();
 
-	var objectName = $(e.currentTarget).attr('data-recipe-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-id');
 	var button = $(this);
 	var servings = $(e.currentTarget).attr('data-mealplan-servings');
 
 	bootbox.confirm({
@@ -665,11 +665,11 @@ $(document).on('click', '.recipe-consume-button', function(e)
 {
 	// Remove the focus from the current button
 	// to prevent that the tooltip stays until clicked anywhere else
 	document.activeElement.blur();
 
-	var objectName = $(e.currentTarget).attr('data-recipe-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-id');
 	var servings = $(e.currentTarget).attr('data-mealplan-servings');
 
 	bootbox.confirm({
 		message: __t('Are you sure to consume all ingredients needed by recipe "%s" (ingredients marked with "check only if a single unit is in stock" will be ignored)?', objectName),
diff --git a/public/viewjs/productgroups.js b/public/viewjs/productgroups.js
index af64c63..4058789 100644
--- a/public/viewjs/productgroups.js
+++ b/public/viewjs/productgroups.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	groupsTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.product-group-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-group-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-group-name'));
 	var objectId = $(e.currentTarget).attr('data-group-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete product group "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/products.js b/public/viewjs/products.js
index 5934c4b..77d1da7 100644
--- a/public/viewjs/products.js
+++ b/public/viewjs/products.js
@@ -36,11 +36,11 @@ if (typeof GetUriParam("product-group") !== "undefined")
 	$("#product-group-filter").trigger("change");
 }
 
 $(document).on('click', '.product-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-product-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-product-name'));
 	var objectId = $(e.currentTarget).attr('data-product-id');
 
 	Grocy.Api.Get('stock/products/' + objectId,
 		function(productDetails)
 		{
diff --git a/public/viewjs/quantityunits.js b/public/viewjs/quantityunits.js
index 9d5766a..8b5cc35 100644
--- a/public/viewjs/quantityunits.js
+++ b/public/viewjs/quantityunits.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	quantityUnitsTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.quantityunit-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-quantityunit-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-quantityunit-name'));
 	var objectId = $(e.currentTarget).attr('data-quantityunit-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete quantity unit "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js
index 9a36d7e..09501c4 100644
--- a/public/viewjs/recipeform.js
+++ b/public/viewjs/recipeform.js
@@ -124,11 +124,11 @@ $('#recipe-form input').keydown(function(event)
 	}
 });
 
 $(document).on('click', '.recipe-pos-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-recipe-pos-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-pos-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete recipe ingredient "%s"?', objectName),
 		closeButton: false,
@@ -161,11 +161,11 @@ $(document).on('click', '.recipe-pos-delete-button', function(e)
 	});
 });
 
 $(document).on('click', '.recipe-include-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-recipe-include-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-include-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-include-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to remove the included recipe "%s"?', objectName),
 		closeButton: false,
@@ -198,11 +198,11 @@ $(document).on('click', '.recipe-include-delete-button', function(e)
 	});
 });
 
 $(document).on('click', '.recipe-pos-show-note-button', function(e)
 {
-	var note = $(e.currentTarget).attr('data-recipe-pos-note');
+	var note = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-note'));
 
 	bootbox.alert(note);
 });
 
 $(document).on('click', '.recipe-pos-edit-button', function(e)
diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js
index 56d03d2..d176c43 100644
--- a/public/viewjs/recipes.js
+++ b/public/viewjs/recipes.js
@@ -79,11 +79,11 @@ $("#status-filter").on("change", function()
 
 $(".recipe-delete").on('click', function(e)
 {
 	e.preventDefault();
 
-	var objectName = $(e.currentTarget).attr('data-recipe-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete recipe "%s"?', objectName),
 		closeButton: false,
@@ -116,11 +116,11 @@ $(".recipe-delete").on('click', function(e)
 	});
 });
 
 $(document).on('click', '.recipe-shopping-list', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-recipe-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to put all missing ingredients for recipe "%s" on the shopping list?', objectName) + "<br><br>" + __t("Uncheck ingredients to not put them on the shopping list") + ":" + $("#missing-recipe-pos-list")[0].outerHTML.replace("d-none", ""),
 		closeButton: false,
@@ -162,11 +162,11 @@ $(document).on('click', '.recipe-shopping-list', function(e)
 	});
 });
 
 $(".recipe-consume").on('click', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-recipe-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
 	var objectId = $(e.currentTarget).attr('data-recipe-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to consume all ingredients needed by recipe "%s" (ingredients marked with "check only if a single unit is in stock" will be ignored)?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js
index 3244e25..b1c7843 100644
--- a/public/viewjs/shoppinglist.js
+++ b/public/viewjs/shoppinglist.js
@@ -75,11 +75,11 @@ $(".status-filter-message").on("click", function()
 	$("#status-filter").trigger("change");
 });
 
 $("#delete-selected-shopping-list").on("click", function()
 {
-	var objectName = $("#selected-shopping-list option:selected").text();
+	var objectName = SanitizeHtml($("#selected-shopping-list option:selected").text());
 	var objectId = $("#selected-shopping-list").val();
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete shopping list "%s"?', objectName),
 		closeButton: false,
@@ -156,11 +156,11 @@ $(document).on('click', '#add-products-below-min-stock-amount', function(e)
 });
 
 $(document).on('click', '#clear-shopping-list', function(e)
 {
 	bootbox.confirm({
-		message: __t('Are you sure to empty shopping list "%s"?', $("#selected-shopping-list option:selected").text()),
+		message: __t('Are you sure to empty shopping list "%s"?', SanitizeHtml($("#selected-shopping-list option:selected").text())),
 		closeButton: false,
 		buttons: {
 			confirm: {
 				label: __t('Yes'),
 				className: 'btn-success'
diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js
index 5843ba0..c5bdb6e 100644
--- a/public/viewjs/shoppinglocations.js
+++ b/public/viewjs/shoppinglocations.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	locationsTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.shoppinglocation-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-shoppinglocation-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-shoppinglocation-name'));
 	var objectId = $(e.currentTarget).attr('data-shoppinglocation-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete store "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/taskcategories.js b/public/viewjs/taskcategories.js
index d3953b4..025bfc6 100644
--- a/public/viewjs/taskcategories.js
+++ b/public/viewjs/taskcategories.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	categoriesTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.task-category-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-category-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-category-name'));
 	var objectId = $(e.currentTarget).attr('data-category-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete task category "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js
index 7e93c03..e8a84d9 100644
--- a/public/viewjs/tasks.js
+++ b/public/viewjs/tasks.js
@@ -117,11 +117,11 @@ $(document).on('click', '.undo-task-button', function(e)
 
 $(document).on('click', '.delete-task-button', function(e)
 {
 	e.preventDefault();
 
-	var objectName = $(e.currentTarget).attr('data-task-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-task-name'));
 	var objectId = $(e.currentTarget).attr('data-task-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete task "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/userentities.js b/public/viewjs/userentities.js
index 74bbaf9..e6c5e85 100644
--- a/public/viewjs/userentities.js
+++ b/public/viewjs/userentities.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	userentitiesTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.userentity-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-userentity-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-userentity-name'));
 	var objectId = $(e.currentTarget).attr('data-userentity-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete userentity "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/userfields.js b/public/viewjs/userfields.js
index 5d5c271..b9b69b2 100644
--- a/public/viewjs/userfields.js
+++ b/public/viewjs/userfields.js
@@ -31,11 +31,11 @@ $("#entity-filter").on("change", function()
 	$("#new-userfield-button").attr("href", U("/userfield/new?entity=" + value));
 });
 
 $(document).on('click', '.userfield-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-userfield-name');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-userfield-name'));
 	var objectId = $(e.currentTarget).attr('data-userfield-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete user field "%s"?', objectName),
 		closeButton: false,
diff --git a/public/viewjs/users.js b/public/viewjs/users.js
index d0d4c67..bc17bf8 100644
--- a/public/viewjs/users.js
+++ b/public/viewjs/users.js
@@ -19,11 +19,11 @@ $("#search").on("keyup", Delay(function()
 	usersTable.search(value).draw();
 }, 200));
 
 $(document).on('click', '.user-delete-button', function(e)
 {
-	var objectName = $(e.currentTarget).attr('data-user-username');
+	var objectName = SanitizeHtml($(e.currentTarget).attr('data-user-username'));
 	var objectId = $(e.currentTarget).attr('data-user-id');
 
 	bootbox.confirm({
 		message: __t('Are you sure to delete user "%s"?', objectName),
 		closeButton: false,

Vulnerable code: 	var objectName = $(e.currentTarget).attr('data-battery-name');
	var objectName = $(e.currentTarget).attr('data-chore-name');
				message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input),
	var objectName = $(e.currentTarget).attr('data-equipment-name');
	var objectName = $(e.currentTarget).attr('data-location-name');
	var objectName = $(e.currentTarget).attr('data-recipe-name');
	var objectName = $(e.currentTarget).attr('data-recipe-name');
	var objectName = $(e.currentTarget).attr('data-group-name');
	var objectName = $(e.currentTarget).attr('data-product-name');
	var objectName = $(e.currentTarget).attr('data-quantityunit-name');
	var objectName = $(e.currentTarget).attr('data-recipe-pos-name');
	var objectName = $(e.currentTarget).attr('data-recipe-include-name');
	var note = $(e.currentTarget).attr('data-recipe-pos-note');
	var objectName = $(e.currentTarget).attr('data-recipe-name');
	var objectName = $(e.currentTarget).attr('data-recipe-name');
	var objectName = $(e.currentTarget).attr('data-recipe-name');
	var objectName = $("#selected-shopping-list option:selected").text();
		message: __t('Are you sure to empty shopping list "%s"?', $("#selected-shopping-list option:selected").text()),
	var objectName = $(e.currentTarget).attr('data-shoppinglocation-name');
	var objectName = $(e.currentTarget).attr('data-category-name');
	var objectName = $(e.currentTarget).attr('data-task-name');
	var objectName = $(e.currentTarget).attr('data-userentity-name');
	var objectName = $(e.currentTarget).attr('data-userfield-name');
	var objectName = $(e.currentTarget).attr('data-user-username');
Fixed code: 



function SanitizeHtml(input)
{
	return $("<div/>").text(input).html();
}
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-battery-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-chore-name'));
				message: __t('"%s" could not be resolved to a product, how do you want to proceed?', SanitizeHtml(input)),
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-equipment-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-location-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-group-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-product-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-quantityunit-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-include-name'));
	var note = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-note'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name'));
	var objectName = SanitizeHtml($("#selected-shopping-list option:selected").text());
		message: __t('Are you sure to empty shopping list "%s"?', SanitizeHtml($("#selected-shopping-list option:selected").text())),
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-shoppinglocation-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-category-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-task-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-userentity-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-userfield-name'));
	var objectName = SanitizeHtml($(e.currentTarget).attr('data-user-username'));