diff --git a/api/utils/common.js b/api/utils/common.js index 05119c59521..3181983d06c 100644 --- a/api/utils/common.js +++ b/api/utils/common.js @@ -124,6 +124,31 @@ function getJSON(val) { return ret; } +/** + * Whether a value is a plain object or an array, and so should be walked rather than escaped + * as a scalar. + * + * Tested by prototype identity rather than by reading value.constructor. `constructor` is an + * ordinary property name, so a JSON body can carry its own: {"constructor": true, ...} makes + * value.constructor evaluate to true, which used to fail the check and return the object with + * its keys and values unescaped. Since escape_html_entities is the replacer for every + * returnOutput and returnMessage, that turned any user controlled property name into markup + * wherever a response is rendered. + * + * Prototype identity cannot be spoofed by an own property, and it keeps the original intent: + * ObjectIDs, Dates and other class instances are still escaped as scalars rather than walked. + * + * @param {Any} value - value under inspection + * @returns {boolean} true when it should be recursed into + */ +function isPlainContainer(value) { + if (Array.isArray(value)) { + return true; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + /** * Escape special characters in the given value, may be nested object * @param {string} key - key of the value @@ -132,7 +157,7 @@ function getJSON(val) { * @returns {any} escaped value **/ function escape_html_entities(key, value, more) { - if (typeof value === 'object' && value && (value.constructor === Object || value.constructor === Array)) { + if (typeof value === 'object' && value && isPlainContainer(value)) { if (Array.isArray(value)) { let replacement = []; for (let k = 0; k < value.length; k++) { diff --git a/plugins/compliance-hub/frontend/public/javascripts/countly.views.js b/plugins/compliance-hub/frontend/public/javascripts/countly.views.js index ae448f49622..37132627298 100644 --- a/plugins/compliance-hub/frontend/public/javascripts/countly.views.js +++ b/plugins/compliance-hub/frontend/public/javascripts/countly.views.js @@ -13,6 +13,13 @@ this.$store.dispatch("countlyConsentManager/fetchUserDataResource"); }, methods: { + // Consent feature names are sdk supplied and rendered as text. The api + // escapes them on the way out, so undo that to keep the label readable. + consentFeatures: function(features) { + return (features || []).map(function(name) { + return countlyCommon.unescapeHtml(name); + }).join(","); + }, deleteUserData: function(uid) { var self = this; CountlyHelpers.confirm(this.i18n("app-users.delete-userdata-confirm"), "popStyleGreen", function(result) { diff --git a/plugins/compliance-hub/frontend/public/templates/user.html b/plugins/compliance-hub/frontend/public/templates/user.html index 06b24d2b8f2..8883aa31507 100644 --- a/plugins/compliance-hub/frontend/public/templates/user.html +++ b/plugins/compliance-hub/frontend/public/templates/user.html @@ -30,9 +30,9 @@