Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion api/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ common.decode_html = function(string) {
return string;
};

/**
* 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
Expand All @@ -121,7 +146,7 @@ common.decode_html = function(string) {
* @returns {vary} 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++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(",");
},
switchToConsentHistory: function(uid) {
window.location.hash = "#/manage/compliance/history/" + uid;
},
Expand Down
4 changes: 2 additions & 2 deletions plugins/compliance-hub/frontend/public/templates/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<template v-slot="rowScope">
<div v-if="rowScope.row.consent">
<p class="color-primary-green text-smaller text-uppercase bu-mb-1" style="margin-top: 0px; font-weight: 700">{{i18n("consent.opt-i")}}</p>
<span class="text-small bu-mb-4" v-html="rowScope.row.optin.join(',')"></span>
<span class="text-small bu-mb-4">{{consentFeatures(rowScope.row.optin)}}</span>
<p class="color-red-100 text-smaller text-uppercase bu-mb-1" style="font-weight: 700;">{{i18n("consent.opt-o")}}</p>
<span class="text-small" v-html="rowScope.row.optout.join(',')"></span>
<span class="text-small">{{consentFeatures(rowScope.row.optout)}}</span>
</div>
<div v-if="!rowScope.row.consent">
-
Expand Down
Loading