From 4c32a5d0ecab13f36ebf2a0ac62e2b19c4c021de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Fri, 17 May 2024 15:42:53 +0200 Subject: [PATCH] sanitize user input when passing it to `innerHTML` in the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * use dompurify library for that * replaced many `innerHTML` assignments with `textContent` Signed-off-by: Thomas Jäckle --- ui/modules/connections/connections.ts | 12 +++--- ui/modules/connections/connectionsMonitor.ts | 14 +++--- ui/modules/environments/environments.ts | 6 +-- ui/modules/operations/piggyback.ts | 10 ++--- ui/modules/operations/servicesLogging.ts | 6 +-- ui/modules/operations/templates.ts | 8 ++-- ui/modules/policies/policies.ts | 4 +- ui/modules/policies/policiesEntries.ts | 6 +-- ui/modules/policies/policiesImports.ts | 10 ++--- ui/modules/policies/policiesResources.ts | 6 +-- ui/modules/policies/policiesSubjects.ts | 6 +-- ui/modules/things/attributes.ts | 2 +- ui/modules/things/featureMessages.ts | 4 +- ui/modules/things/features.ts | 2 +- ui/modules/things/fields.ts | 8 ++-- ui/modules/things/messagesIncoming.ts | 4 +- ui/modules/things/searchFilter.ts | 8 ++-- ui/modules/things/thingMessages.ts | 4 +- ui/modules/things/thingsCRUD.ts | 2 +- ui/modules/things/thingsSearch.ts | 30 +++++++------ ui/modules/utils.ts | 45 ++++++++------------ ui/modules/utils/tableFilter.ts | 4 +- ui/package-lock.json | 6 +++ ui/package.json | 1 + 24 files changed, 101 insertions(+), 107 deletions(-) diff --git a/ui/modules/connections/connections.ts b/ui/modules/connections/connections.ts index f44eb03419..b87c3ef9e8 100644 --- a/ui/modules/connections/connections.ts +++ b/ui/modules/connections/connections.ts @@ -16,7 +16,7 @@ /* eslint-disable require-jsdoc */ import * as API from '../api.js'; import * as Utils from '../utils.js'; -import {TabHandler} from '../utils/tabHandler.js'; +import { TabHandler } from '../utils/tabHandler.js'; import connectionsHTML from './connections.html'; const observers = []; @@ -57,7 +57,7 @@ export function setConnection(connection, isNewConnection = false) { } export function loadConnections() { - dom.tbodyConnections.innerHTML = ''; + dom.tbodyConnections.textContent = ''; let connectionSelected = false; API.callConnectionsAPI('listConnections', (connections) => { connections.forEach((connection) => { @@ -66,15 +66,15 @@ export function loadConnections() { row.id = id; if (API.env() === 'ditto_2') { API.callConnectionsAPI('retrieveConnection', (dittoConnection) => { - row.insertCell(0).innerHTML = dittoConnection.name; + row.insertCell(0).textContent = dittoConnection.name; }, id); } else { - row.insertCell(0).innerHTML = connection.name ? connection.name : id; + row.insertCell(0).textContent = connection.name ? connection.name : id; } API.callConnectionsAPI('retrieveStatus', (status) => { - row.insertCell(-1).innerHTML = status.liveStatus; - row.insertCell(-1).innerHTML = status.recoveryStatus; + row.insertCell(-1).textContent = status.liveStatus; + row.insertCell(-1).textContent = status.recoveryStatus; }, id); if (id === selectedConnectionId) { diff --git a/ui/modules/connections/connectionsMonitor.ts b/ui/modules/connections/connectionsMonitor.ts index c2637cdb39..8611be4cfe 100644 --- a/ui/modules/connections/connectionsMonitor.ts +++ b/ui/modules/connections/connectionsMonitor.ts @@ -10,13 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {JSONPath} from 'jsonpath-plus'; - import * as API from '../api.js'; import * as Utils from '../utils.js'; -import * as Connections from './connections.js'; -import { TableFilter } from '../utils/tableFilter.js'; import { FilterType, Term } from '../utils/basicFilters.js'; +import { TableFilter } from '../utils/tableFilter.js'; +import * as Connections from './connections.js'; /* eslint-disable prefer-const */ /* eslint-disable max-len */ /* eslint-disable no-invalid-this */ @@ -112,7 +110,7 @@ function onEnableConnectionLogsClick() { function retrieveConnectionMetrics() { Utils.assert(selectedConnectionId, 'Please select a connection', dom.tableValidationConnections); - dom.tbodyConnectionMetrics.innerHTML = ''; + dom.tbodyConnectionMetrics.textContent = ''; API.callConnectionsAPI('retrieveConnectionMetrics', (response) => { if (response.connectionMetrics) { Object.keys(response.connectionMetrics).forEach((direction) => { @@ -154,7 +152,7 @@ function retrieveConnectionLogs() { } function fillConnectionLogsTable() { - dom.tbodyConnectionLogs.innerHTML = ''; + dom.tbodyConnectionLogs.textContent = ''; connectionLogDetail.setValue(''); filteredLogs = dom.tableFilterConnectionLogs.filterItems(connectionLogs); @@ -188,8 +186,8 @@ function onConnectionChange(connection, isNewConnection = true) { selectedConnectionId = connection ? connection.id : null; connectionStatusDetail.setValue(''); connectionLogDetail.setValue(''); - dom.tbodyConnectionMetrics.innerHTML = ''; - dom.tbodyConnectionLogs.innerHTML = ''; + dom.tbodyConnectionMetrics.textContent = ''; + dom.tbodyConnectionLogs.textContent = ''; if (!isNewConnection && connection && connection.id) { retrieveConnectionLogs(); } diff --git a/ui/modules/environments/environments.ts b/ui/modules/environments/environments.ts index 33b454a3dd..be8ac34d5d 100644 --- a/ui/modules/environments/environments.ts +++ b/ui/modules/environments/environments.ts @@ -11,13 +11,13 @@ * SPDX-License-Identifier: EPL-2.0 */ +import * as Utils from '../utils.js'; /* eslint-disable arrow-parens */ /* eslint-disable prefer-const */ /* eslint-disable require-jsdoc */ import * as Authorization from './authorization.js'; -import * as Utils from '../utils.js'; -import defaultTemplates from './environmentTemplates.json'; import environmentsHTML from './environments.html'; +import defaultTemplates from './environmentTemplates.json'; const URL_PRIMARY_ENVIRONMENT_NAME = 'primaryEnvironmentName'; @@ -222,7 +222,7 @@ export function environmentsJsonChanged(modifiedField = null) { } function updateEnvTable() { - dom.tbodyEnvironments.innerHTML = ''; + dom.tbodyEnvironments.textContent = ''; Object.keys(environments).forEach((key) => { Utils.addTableRow(dom.tbodyEnvironments, key, key === selectedEnvName); }); diff --git a/ui/modules/operations/piggyback.ts b/ui/modules/operations/piggyback.ts index 06aad53c69..bbe8cb0b3a 100644 --- a/ui/modules/operations/piggyback.ts +++ b/ui/modules/operations/piggyback.ts @@ -16,10 +16,10 @@ /* eslint-disable require-jsdoc */ import * as API from '../api.js'; import * as Utils from '../utils.js'; +import { TabHandler } from '../utils/tabHandler.js'; import piggybackHTML from './piggyback.html'; import piggybackPlaceholders from './piggybackPlaceholders.json'; import * as Templates from './templates.js'; -import {TabHandler} from '../utils/tabHandler.js'; const EDITOR_INVALID_JSON_MNSSAGE = 'Invalid json!' const HEADER_IS_REQUIRED_MESSAGE = 'Headers field is required!'; @@ -205,7 +205,7 @@ function hasEditorError(editorSession) { async function submitPiggybackCommand() { if (isCommandValid()) { - dom.responseStatus.innerHTML = REQUEST_IN_PROGRESS_MESSAGE; + dom.responseStatus.textContent = REQUEST_IN_PROGRESS_MESSAGE; aceResponse.setValue('', -1); let path = buildPath( dom.serviceSelector.value, @@ -228,19 +228,19 @@ async function submitPiggybackCommand() { } catch (err) { onRequestDone(); aceResponse.setValue(err.message, -1); - dom.responseStatus.innerHTML = REQUEST_ERROR_MESSAGE; + dom.responseStatus.textContent = REQUEST_ERROR_MESSAGE; } }); promise.then((result: any) => { onRequestDone(); result.json().then(resultJson => { aceResponse.setValue(Utils.stringifyPretty(resultJson), -1); - dom.responseStatus.innerHTML = result.status; + dom.responseStatus.textContent = result.status; }); }).catch(err => { onRequestDone(); aceResponse.setValue(err.message, -1); - dom.responseStatus.innerHTML = REQUEST_ERROR_MESSAGE; + dom.responseStatus.textContent = REQUEST_ERROR_MESSAGE; }); } diff --git a/ui/modules/operations/servicesLogging.ts b/ui/modules/operations/servicesLogging.ts index 75a9244f4e..c40aa1c5ef 100644 --- a/ui/modules/operations/servicesLogging.ts +++ b/ui/modules/operations/servicesLogging.ts @@ -16,7 +16,7 @@ /* eslint-disable require-jsdoc */ import * as API from '../api.js'; import * as Utils from '../utils.js'; -import {TabHandler} from '../utils/tabHandler.js'; +import { TabHandler } from '../utils/tabHandler.js'; import servicesLoggingHTML from './servicesLogging.html'; let dom = { @@ -47,12 +47,12 @@ function loadAllLogLevels() { API.callDittoREST('GET', '/devops/logging', null, null, false, true) .then((result) => createLoggerView(result)) .catch((error) => { - dom.divLoggers.innerHTML = ''; + dom.divLoggers.textContent = ''; }); } function createLoggerView(allLogLevels) { - dom.divLoggers.innerHTML = ''; + dom.divLoggers.textContent = ''; type LogLevel = { loggerConfigs?: object[] diff --git a/ui/modules/operations/templates.ts b/ui/modules/operations/templates.ts index 6a73ed9e90..95e6c46936 100644 --- a/ui/modules/operations/templates.ts +++ b/ui/modules/operations/templates.ts @@ -11,9 +11,9 @@ * SPDX-License-Identifier: EPL-2.0 */ import * as Utils from '../utils.js'; -import templatesHTML from './templates.html'; -import templatesByService from './piggybackTemplates.json'; import * as Piggyback from './piggyback.js'; +import templatesByService from './piggybackTemplates.json'; +import templatesHTML from './templates.html'; const dom = { templateServiceSelector: null, @@ -88,9 +88,9 @@ function onTemplateSelected() { if (selectedTemplate) { let templateBody = buildPiggybackCommand(selectedTemplate.targetActorSelection, selectedTemplate.headers, selectedTemplate.command); - dom.commandPreview.innerHTML = Utils.stringifyPretty(templateBody); + dom.commandPreview.textContent = Utils.stringifyPretty(templateBody); } else { - dom.commandPreview.innerHTML = ""; + dom.commandPreview.textContent = ''; } } diff --git a/ui/modules/policies/policies.ts b/ui/modules/policies/policies.ts index 7e61370495..0f0fd04582 100644 --- a/ui/modules/policies/policies.ts +++ b/ui/modules/policies/policies.ts @@ -111,7 +111,7 @@ function onThingChanged(thing) { } function refreshWhoAmI() { - dom.tbodyWhoami.innerHTML = ''; + dom.tbodyWhoami.textContent = ''; API.callDittoREST('GET', '/whoami') .then((whoamiResult) => { whoamiResult.subjects.forEach((subject) => { @@ -166,7 +166,7 @@ export function updateRecentPolicies(policyId: String) { function onEnvironmentChanged(modifiedField: String) { Environments.current()['recentPolicyIds'] = Environments.current()['recentPolicyIds'] || []; - dom.tbodyRecentPolicies.innerHTML = ''; + dom.tbodyRecentPolicies.textContent = ''; Environments.current().recentPolicyIds.forEach(entry => { Utils.addTableRow(dom.tbodyRecentPolicies, entry, thePolicy && thePolicy.policyId === entry, entry); }); diff --git a/ui/modules/policies/policiesEntries.ts b/ui/modules/policies/policiesEntries.ts index cdd29d8eea..b456fd5cf5 100644 --- a/ui/modules/policies/policiesEntries.ts +++ b/ui/modules/policies/policiesEntries.ts @@ -11,10 +11,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import * as Utils from '../utils.js'; import * as API from '../api.js'; -import { Observable } from '../utils/observable.js'; +import * as Utils from '../utils.js'; import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; +import { Observable } from '../utils/observable.js'; import * as Policies from './policies.js'; export let observable = Observable(); @@ -101,7 +101,7 @@ function putOrDeletePolicyEntry(entry, value, onSuccess) { }; function onPolicyChanged(policy: Policies.Policy) { - dom.tbodyPolicyEntries.innerHTML = ''; + dom.tbodyPolicyEntries.textContent = ''; dom.crudEntry.idValue = null; dom.crudEntry.editDisabled = (policy === null); diff --git a/ui/modules/policies/policiesImports.ts b/ui/modules/policies/policiesImports.ts index 831bd3dacb..739ecea1cf 100644 --- a/ui/modules/policies/policiesImports.ts +++ b/ui/modules/policies/policiesImports.ts @@ -11,12 +11,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import * as ace from 'ace-builds/src-noconflict/ace'; -import * as Utils from '../utils.js'; import * as API from '../api.js'; -import * as Policies from './policies.js'; -import * as PolicyEntries from './policiesEntries.js'; +import * as Utils from '../utils.js'; import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; +import * as Policies from './policies.js'; let selectedImport: string; @@ -111,7 +109,7 @@ function setExplicitCheckboxesDisabledState(disabled: boolean) { } function onPolicyChanged(policy: Policies.Policy) { - dom.tbodyPolicyImports.innerHTML = ''; + dom.tbodyPolicyImports.textContent = ''; dom.crudImport.idValue = null; dom.crudImport.editDisabled = (policy === null); @@ -145,7 +143,7 @@ function onPolicyChanged(policy: Policies.Policy) { async function setImport(importedPolicyId: string) { dom.crudImport.idValue = importedPolicyId; - dom.tbodyPolicyImportEntries.innerHTML = ''; + dom.tbodyPolicyImportEntries.textContent = ''; if (importedPolicyId) { const importedPolicy: Policies.Policy = await API.callDittoREST('GET', '/policies/' + importedPolicyId) diff --git a/ui/modules/policies/policiesResources.ts b/ui/modules/policies/policiesResources.ts index e23be1df8e..2e23cba672 100644 --- a/ui/modules/policies/policiesResources.ts +++ b/ui/modules/policies/policiesResources.ts @@ -12,12 +12,12 @@ */ import * as ace from 'ace-builds/src-noconflict/ace'; -import * as Utils from '../utils.js'; import * as API from '../api.js'; +import * as Utils from '../utils.js'; +import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; import * as Policies from './policies.js'; import * as PolicyEntries from './policiesEntries.js'; import resourceTemplates from './resourceTemplates.json'; -import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; let selectedResource: string; @@ -138,7 +138,7 @@ function onEditToggleResource(event: CustomEvent) { function onEntryChanged(entryLabel: string) { selectedResource = null; - dom.tbodyPolicyResources.innerHTML = ''; + dom.tbodyPolicyResources.textContent = ''; dom.crudResource.idValue = null; resourceEditor.setValue(''); dom.crudResource.editDisabled = (entryLabel === null); diff --git a/ui/modules/policies/policiesSubjects.ts b/ui/modules/policies/policiesSubjects.ts index 256b8b7ae0..e4c065550d 100644 --- a/ui/modules/policies/policiesSubjects.ts +++ b/ui/modules/policies/policiesSubjects.ts @@ -12,11 +12,11 @@ */ import * as ace from 'ace-builds/src-noconflict/ace'; -import * as Utils from '../utils.js'; import * as API from '../api.js'; +import * as Utils from '../utils.js'; +import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; import * as Policies from './policies.js'; import * as PolicyEntries from './policiesEntries.js'; -import { CrudOperation, CrudToolbar } from '../utils/crudToolbar.js'; let selectedSubject: string; @@ -121,7 +121,7 @@ function onEditToggleSubject(event: CustomEvent) { function onEntryChanged(entryLabel: string) { selectedSubject = null; - dom.tbodyPolicySubjects.innerHTML = ''; + dom.tbodyPolicySubjects.textContent = ''; dom.crudSubject.idValue = null; dom.crudSubject.editDisabled = (entryLabel === null); subjectEditor.setValue(''); diff --git a/ui/modules/things/attributes.ts b/ui/modules/things/attributes.ts index 5651aaa87a..68f3747f70 100644 --- a/ui/modules/things/attributes.ts +++ b/ui/modules/things/attributes.ts @@ -120,7 +120,7 @@ function refreshAttribute(thing, attributePath = null) { function onThingChanged(thing) { dom.crudAttribute.editDisabled = (thing === null); - dom.tbodyAttributes.innerHTML = ''; + dom.tbodyAttributes.textContent = ''; let count = 0; let thingHasAttribute = false; if (thing && thing.attributes) { diff --git a/ui/modules/things/featureMessages.ts b/ui/modules/things/featureMessages.ts index cf23a012a1..684010f25c 100644 --- a/ui/modules/things/featureMessages.ts +++ b/ui/modules/things/featureMessages.ts @@ -163,12 +163,12 @@ function clearAllFields() { dom.inputMessageTimeout.value = '10'; acePayload.setValue(''); aceResponse.setValue(''); - dom.ulMessageTemplates.innerHTML = ''; + dom.ulMessageTemplates.textContent = ''; dom.buttonMessageSend.disabled = !theFeatureId || theFeatureId === ''; } function refillTemplates() { - dom.ulMessageTemplates.innerHTML = ''; + dom.ulMessageTemplates.textContent = ''; Utils.addDropDownEntry(dom.ulMessageTemplates, 'Saved message templates', true); if (theFeatureId && Environments.current().messageTemplates[theFeatureId]) { Utils.addDropDownEntries( diff --git a/ui/modules/things/features.ts b/ui/modules/things/features.ts index 90535f8473..5ad609b8bd 100644 --- a/ui/modules/things/features.ts +++ b/ui/modules/things/features.ts @@ -219,7 +219,7 @@ function refreshFeature(thing, featureId = null) { function onThingChanged(thing) { dom.crudFeature.editDisabled = (thing === null); // Update features table - dom.tbodyFeatures.innerHTML = ''; + dom.tbodyFeatures.textContent = ''; let count = 0; let thingHasFeature = false; if (thing && thing.features) { diff --git a/ui/modules/things/fields.ts b/ui/modules/things/fields.ts index 66ce01493e..c62c309f9b 100644 --- a/ui/modules/things/fields.ts +++ b/ui/modules/things/fields.ts @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {Modal} from 'bootstrap'; +import { Modal } from 'bootstrap'; import * as Environments from '../environments/environments.js'; import * as Utils from '../utils.js'; @@ -165,14 +165,14 @@ function onEnvironmentChanged() { * (Re-)Initializes the fieldlist in the UI */ function updateFieldList() { - dom.fieldList.innerHTML = ''; + dom.fieldList.textContent = ''; theFieldIndex = -1; Environments.current().fieldList.forEach((field, i) => { const fieldSelected = dom.fieldPath.value === field.path; const row = dom.fieldList.insertRow(); Utils.addCheckboxToRow(row, i, field.active, false, toggleFieldActiveEventHandler); - row.insertCell(-1).innerHTML = field.path; - row.insertCell(-1).innerHTML = field['label'] ? field.label : null; + row.insertCell(-1).textContent = field.path; + row.insertCell(-1).textContent = field['label'] ? field.label : null; if (fieldSelected) { theFieldIndex = i; row.classList.add('table-active'); diff --git a/ui/modules/things/messagesIncoming.ts b/ui/modules/things/messagesIncoming.ts index 271c9e8dba..1b8974c5cf 100644 --- a/ui/modules/things/messagesIncoming.ts +++ b/ui/modules/things/messagesIncoming.ts @@ -113,7 +113,7 @@ function onResetMessagesClick() { messages = []; filteredMessages = []; dom.badgeMessageIncomingCount.textContent = ''; - dom.tbodyMessagesIncoming.innerHTML = ''; + dom.tbodyMessagesIncoming.textContent = ''; messageDetail.setValue(''); } @@ -188,7 +188,7 @@ function createFilterOptions(thing?: any): [Term?] { } function onMessageFilterChange(event: CustomEvent) { - dom.tbodyMessagesIncoming.innerHTML = ''; + dom.tbodyMessagesIncoming.textContent = ''; filteredMessages = dom.tableFilterMessagesIncoming.filterItems(messages); filteredMessages.forEach((entry) => addTableRow(entry)); Utils.updateCounterBadge(dom.badgeMessageIncomingCount, messages, filteredMessages); diff --git a/ui/modules/things/searchFilter.ts b/ui/modules/things/searchFilter.ts index c5c150dfe8..df725266f6 100644 --- a/ui/modules/things/searchFilter.ts +++ b/ui/modules/things/searchFilter.ts @@ -61,8 +61,7 @@ export async function ready() { }); dom.searchThings.onclick = () => { - fillHistory(dom.searchFilterEdit.value); - ThingsSearch.searchTriggered(dom.searchFilterEdit.value); + ThingsSearch.searchTriggered(dom.searchFilterEdit.value, () => fillHistory(dom.searchFilterEdit.value)); }; dom.searchFavorite.onclick = () => { @@ -74,8 +73,7 @@ export async function ready() { dom.searchFilterEdit.onkeyup = (event) => { if ((event.key === 'Enter' || event.code === 13) && dom.searchFilterEdit.value.indexOf(FILTER_PLACEHOLDER) < 0) { - fillHistory(dom.searchFilterEdit.value); - ThingsSearch.searchTriggered(dom.searchFilterEdit.value); + ThingsSearch.searchTriggered(dom.searchFilterEdit.value, () => fillHistory(dom.searchFilterEdit.value)); } else { clearTimeout(keyStrokeTimeout); keyStrokeTimeout = setTimeout(checkIfFavorite, 1000); @@ -107,7 +105,7 @@ function fillSearchFilterEdit(fillString) { checkIfFavorite(); const filterEditNeeded = Utils.checkAndMarkInInput(dom.searchFilterEdit, FILTER_PLACEHOLDER); if (!filterEditNeeded) { - ThingsSearch.searchTriggered(dom.searchFilterEdit.value); + ThingsSearch.searchTriggered(dom.searchFilterEdit.value, () => null); } } diff --git a/ui/modules/things/thingMessages.ts b/ui/modules/things/thingMessages.ts index 82ccd3ca57..e54b7f98c1 100644 --- a/ui/modules/things/thingMessages.ts +++ b/ui/modules/things/thingMessages.ts @@ -153,12 +153,12 @@ function clearAllFields() { dom.inputThingMessageTimeout.value = '10'; acePayload.setValue(''); aceResponse.setValue(''); - dom.ulThingMessageTemplates.innerHTML = ''; + dom.ulThingMessageTemplates.textContent = ''; dom.buttonThingMessageSend.disabled = Things.theThing === null; } function refillTemplates() { - dom.ulThingMessageTemplates.innerHTML = ''; + dom.ulThingMessageTemplates.textContent = ''; Utils.addDropDownEntry(dom.ulThingMessageTemplates, 'Saved message templates', true); if (Environments.current().messageTemplates['/']) { Utils.addDropDownEntries( diff --git a/ui/modules/things/thingsCRUD.ts b/ui/modules/things/thingsCRUD.ts index 912fca247b..6f7908600c 100644 --- a/ui/modules/things/thingsCRUD.ts +++ b/ui/modules/things/thingsCRUD.ts @@ -124,7 +124,7 @@ function onThingChanged(thingJson) { updateThingJsonEditor(); function updateThingDetailsTable() { - dom.tbodyThingDetails.innerHTML = ''; + dom.tbodyThingDetails.textContent = ''; if (thingJson) { Utils.addTableRow(dom.tbodyThingDetails, 'thingId', false, thingJson.thingId, thingJson.thingId); Utils.addTableRow(dom.tbodyThingDetails, 'policyId', false, thingJson.policyId, thingJson.policyId); diff --git a/ui/modules/things/thingsSearch.ts b/ui/modules/things/thingsSearch.ts index a5b98394b6..64855c332e 100644 --- a/ui/modules/things/thingsSearch.ts +++ b/ui/modules/things/thingsSearch.ts @@ -20,8 +20,8 @@ import { JSONPath } from 'jsonpath-plus'; import * as API from '../api.js'; import * as Environments from '../environments/environments.js'; - import * as Utils from '../utils.js'; +import { sanitizeHTML } from '../utils.js'; import * as Fields from './fields.js'; import * as Things from './things.js'; import * as ThingsSSE from './thingsSSE.js'; @@ -76,12 +76,14 @@ function onThingsTableClicked(event) { /** * Tests if the search filter is an RQL. If yes, things search is called otherwise just things get * @param {String} filter search filter string containing an RQL or a thingId + * @param rqlFilterCallback a callback to invoke when the passed `filter` was a valid RQL statement */ -export function searchTriggered(filter: string) { +export function searchTriggered(filter: string, rqlFilterCallback: () => void) { lastSearch = filter; const regex = /^(eq\(|ne\(|gt\(|ge\(|lt\(|le\(|in\(|like\(|ilike\(|exists\(|and\(|or\(|not\().*/; if (filter === '' || regex.test(filter)) { searchThings(filter); + rqlFilterCallback(); } else { getThings([filter]); } @@ -105,7 +107,7 @@ export function performLastSearch() { if (lastSearch === 'pinned') { pinnedTriggered(); } else { - searchTriggered(lastSearch); + searchTriggered(lastSearch, () => null); } } @@ -114,15 +116,15 @@ export function performLastSearch() { * @param {Array} thingIds Array of thingIds */ export function getThings(thingIds) { - dom.searchFilterCount.innerHTML = ''; - dom.thingsTableBody.innerHTML = ''; + dom.searchFilterCount.textContent = ''; + dom.thingsTableBody.textContent = ''; const fieldsQueryParameter = Fields.getQueryParameter(); if (thingIds.length > 0) { API.callDittoREST('GET', `/things?${fieldsQueryParameter}&ids=${thingIds}&option=sort(%2BthingId)`) .then((thingJsonArray) => { fillThingsTable(thingJsonArray); - dom.searchFilterCount.innerHTML = '#: ' + thingJsonArray.length; + dom.searchFilterCount.textContent = '#: ' + thingJsonArray.length; notifyAll(thingIds, fieldsQueryParameter); }) .catch((error) => { @@ -137,9 +139,9 @@ export function getThings(thingIds) { function resetAndClearViews(retainThing = false) { theSearchCursor = null; - dom.searchFilterCount.innerHTML = ''; - dom.thingsTableHead.innerHTML = ''; - dom.thingsTableBody.innerHTML = ''; + dom.searchFilterCount.textContent = ''; + dom.thingsTableHead.textContent = ''; + dom.thingsTableBody.textContent = ''; if (!retainThing) { Things.setTheThing(null); } @@ -150,14 +152,14 @@ function resetAndClearViews(retainThing = false) { * @param {String} filter Ditto search filter (rql) */ function countThings(filter: string) { - dom.searchFilterCount.innerHTML = ''; + dom.searchFilterCount.textContent = ''; const namespaces = Environments.current().searchNamespaces API.callDittoREST('GET', '/search/things/count' + ((filter && filter !== '') ? '?filter=' + encodeURIComponent(filter) : '') + ((namespaces && namespaces !== '') ? '&namespaces=' + namespaces : ''), null, null ).then((countResult) => { - dom.searchFilterCount.innerHTML = '#: ' + countResult; + dom.searchFilterCount.textContent = '#: ' + countResult; }).catch((error) => { notifyAll(); }); @@ -210,7 +212,7 @@ function searchThings(filter: string, isMore = false) { function addMoreToThingList() { const moreCell = dom.thingsTableBody.insertRow().insertCell(-1); - moreCell.innerHTML = 'load more...'; + moreCell.textContent = 'load more...'; moreCell.colSpan = dom.thingsTableBody.rows[0].childElementCount; moreCell.style.textAlign = 'center'; moreCell.style.cursor = 'pointer'; @@ -248,7 +250,7 @@ function fillThingsTable(thingsList: any[]) { } function fillHeaderRow() { - dom.thingsTableHead.innerHTML = ''; + dom.thingsTableHead.textContent = ''; // Utils.addCheckboxToRow(dom.thingsTableHead, 'checkboxHead', false, null); Utils.insertHeaderCell(dom.thingsTableHead, ''); Utils.insertHeaderCell(dom.thingsTableHead, 'Thing ID'); @@ -326,7 +328,7 @@ export function updateTableRow(thingUpdateJson) { path: path, }); if (elem.length !== 0) { - cell.innerHTML = elem[0]; + cell.innerHTML = sanitizeHTML(elem[0]); } } }); diff --git a/ui/modules/utils.ts b/ui/modules/utils.ts index ed2716a905..0e6ff76aee 100644 --- a/ui/modules/utils.ts +++ b/ui/modules/utils.ts @@ -15,6 +15,7 @@ import autoComplete from '@tarekraafat/autocomplete.js'; import * as ace from 'ace-builds/src-noconflict/ace'; import { Modal, Toast } from 'bootstrap'; +import * as DOMPurify from 'dompurify'; const dom = { @@ -90,7 +91,7 @@ export function addCheckboxToRow(row: HTMLTableRowElement, id: string, checked: */ export function addCellToRow(row, cellContent, cellTooltip = null, position = -1) { const cell = row.insertCell(position); - cell.innerHTML = cellContent; + cell.textContent = cellContent; cell.setAttribute('data-bs-toggle', 'tooltip'); cell.title = cellTooltip ?? cellContent; return cell; @@ -130,36 +131,17 @@ export function getRowClipboardAction(iconClassMain: string, iconClassFeedback: */ export function insertHeaderCell(row, label) { const th = document.createElement('th'); - th.innerHTML = label; + th.textContent = label; row.appendChild(th); } -/** - * Create a radio button element - * @param {HTMLElement} target target element - * @param {String} groupName group for consecutive added radio buttons - * @param {String} value name of the radio button - * @param {boolean} checked check the radio button - */ -export function addRadioButton(target, groupName, value, checked) { - const radio = document.createElement('div'); - radio.innerHTML = `
- - -
`; - target.appendChild(radio); -} - /** * Create a list of option elements for select element * @param {HTMLSelectElement} target target element (select) * @param {array} options Array of strings to be filled as options */ export function setOptions(target: HTMLSelectElement, options: string[]) { - target.innerHTML = ''; + target.textContent = ''; options.forEach((key) => { const option = document.createElement('option'); option.text = key; @@ -231,6 +213,15 @@ export function getAllElementsById(domObjects: object, searchRoot: DocumentFragm }); } +/** + * Sanitizes the passed unsafeString to be safely used e.g. inside innerHTML without risking XSS. + + * @param unsafeString the string to sanitize. + */ +export function sanitizeHTML(unsafeString: String) { + return DOMPurify.sanitize(unsafeString); +} + /** * Show an error toast * @param {String} message Message for toast @@ -242,11 +233,11 @@ export function showError(message, header = 'Error', status = '') { domToast.classList.add('toast'); domToast.innerHTML = `
- ${header} + ${sanitizeHTML(header)} ${status}
-
${message}
`; +
${sanitizeHTML(message)}
`; dom.toastContainer.appendChild(domToast); domToast.addEventListener("hidden.bs.toast", () => { @@ -278,7 +269,7 @@ export function assert(condition, message, validatedElement = null) { } if (!condition) { if (validatedElement) { - validatedElement.parentNode.getElementsByClassName('invalid-feedback')[0].innerHTML = message; + validatedElement.parentNode.getElementsByClassName('invalid-feedback')[0].textContent = message; validatedElement.classList.add('is-invalid'); } else { showError(message, 'Error'); @@ -311,7 +302,7 @@ let modalConfirm; */ export function confirm(message, action, callback) { modalConfirm = modalConfirm ?? new Modal('#modalConfirm'); - dom.modalBodyConfirm.innerHTML = message; + dom.modalBodyConfirm.textContent = message; dom.buttonConfirmed.innerText = action; dom.buttonConfirmed.onclick = callback; modalConfirm.show(); @@ -362,7 +353,7 @@ export function createAutoComplete(selector, src, placeHolder) { highlight: true, element: (item, data) => { item.style = 'display: flex;'; - item.innerHTML = `${data.key === 'label' ? data.match : data.value.label} + item.innerHTML = `${sanitizeHTML(data.key === 'label' ? data.match : data.value.label)} ${data.key === 'group' ? data.match : data.value.group}`; }, diff --git a/ui/modules/utils/tableFilter.ts b/ui/modules/utils/tableFilter.ts index df47fc5186..da565115b5 100644 --- a/ui/modules/utils/tableFilter.ts +++ b/ui/modules/utils/tableFilter.ts @@ -13,8 +13,8 @@ import { JSONPath } from 'jsonpath-plus'; import * as Utils from '../utils.js'; -import tableFilterHTML from './tableFilter.html'; import { BasicFilters, FilterListener, Term } from './basicFilters.js'; +import tableFilterHTML from './tableFilter.html'; enum Mode { BASIC=0, @@ -81,7 +81,7 @@ export class TableFilter extends HTMLElement implements FilterListener { private fillUIChips() { const div = this.querySelector('[data-bs-theme="dark"]') as HTMLElement; - div.innerHTML = ''; + div.textContent = ''; this.basicFilters.getAllUIs().forEach((chip) => div.appendChild(chip)); } diff --git a/ui/package-lock.json b/ui/package-lock.json index 1f57e16fb0..e92a98936a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,6 +14,7 @@ "ace-builds": "^1.32.2", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.2", + "dompurify": "^3.1.3", "event-source-polyfill": "^1.0.31", "jsonpath-plus": "^7.2.0" }, @@ -2635,6 +2636,11 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.3.tgz", + "integrity": "sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng==" + }, "node_modules/electron-to-chromium": { "version": "1.4.616", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.616.tgz", diff --git a/ui/package.json b/ui/package.json index d092580c00..8008d26f58 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,6 +29,7 @@ "dependencies": { "@popperjs/core": "^2.11.7", "@tarekraafat/autocomplete.js": "^10.2.7", + "dompurify": "^3.1.3", "ace-builds": "^1.32.2", "bootstrap": "^5.3.2", "bootstrap-icons": "^1.11.2",