From b2ae6f92d12206ea185a2e882945a6b69234bf03 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 27 Aug 2025 16:05:20 +0300 Subject: [PATCH 1/7] added Content-Security-Policy header to download image api to prevent malicious code injection --- .../java/org/thingsboard/server/controller/ImageController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/ImageController.java b/application/src/main/java/org/thingsboard/server/controller/ImageController.java index f9ec7fd844..9288484f86 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ImageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ImageController.java @@ -300,6 +300,7 @@ public class ImageController extends BaseController { tbImageService.putETag(cacheKey, descriptor.getEtag()); var result = ResponseEntity.ok() .header("Content-Type", descriptor.getMediaType()) + .header("Content-Security-Policy", "default-src 'none'") .eTag(descriptor.getEtag()); if (!cacheKey.isPublic()) { result From e34a2fe268ad08c15df6fed859751176772967ec Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 23 Sep 2025 17:31:51 +0300 Subject: [PATCH 2/7] fixed /api/alarmsQuery/find api to retrieve entity latest values --- .../query/DefaultEntityQueryService.java | 2 +- .../controller/EntityQueryControllerTest.java | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 45a73072f5..f39769526a 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 1f80c1f379..93d67ccd4b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } + @Test + public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + List temps = new ArrayList<>(); + List deviceNames = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + devices.add(device); + deviceNames.add(device.getName()); + + int temp = i * 10; + temps.add(String.valueOf(temp)); + JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content) + .andExpect(status().isOk()); + Thread.sleep(1); + } + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + String type = "device alarm" + i; + alarm.setType(type); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time"))); + + List alarmFields = new ArrayList<>(); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + List entityFields = new ArrayList<>(); + entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + List latestValues = new ArrayList<>(); + latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + + EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); + deviceTypeFilter.setEntityType(EntityType.DEVICE); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + + PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); + List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps); + + List retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList(); + assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames); + } + private void testCountAlarmsByQuery(List alarms) throws Exception { AlarmCountQuery countQuery = new AlarmCountQuery(); From 599ccdc43c584fee6d0a238133f1e0d1117b33d8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 12 Aug 2025 18:30:42 +0300 Subject: [PATCH 3/7] added NoXss validation --- .../server/common/data/mobile/bundle/MobileAppBundle.java | 3 +++ .../server/common/data/notification/rule/NotificationRule.java | 1 + .../common/data/notification/rule/NotificationRuleConfig.java | 2 ++ .../template/DeliveryMethodNotificationTemplate.java | 2 ++ 4 files changed, 8 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java index 2190972d7e..b023ba89c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @Data @@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData implements HasT private TenantId tenantId; @Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @NoXss @Length(fieldName = "title") private String title; @Schema(description = "Application bundle description.") + @NoXss @Length(fieldName = "description") private String description; @Schema(description = "Android application id") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 81b5e0ccfb..fe87e0f966 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -62,6 +62,7 @@ public class NotificationRule extends BaseData implements Ha @Valid private NotificationRuleRecipientsConfig recipientsConfig; + @Valid private NotificationRuleConfig additionalConfig; private NotificationRuleId externalId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java index 9103086b7c..013c0ae662 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.notification.rule; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; @Data public class NotificationRuleConfig implements Serializable { + @NoXss private String description; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index e660d49bca..d9b9df0fdf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -24,6 +24,7 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.List; @@ -43,6 +44,7 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty + @NoXss protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { From 33df79cd12d5fa6b45e8da3247554f656494c468 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 13 Aug 2025 11:19:16 +0300 Subject: [PATCH 4/7] added sanitize for widget action name on delete --- .../widget/action/manage-widget-actions.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index a9b9d207d0..01404f7c6e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -24,6 +24,7 @@ import { NgZone, OnDestroy, OnInit, + SecurityContext, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -53,6 +54,7 @@ import { import { deepClone } from '@core/utils'; import { hidePageSizePixelValue } from '@shared/models/constants'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-manage-widget-actions', @@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private dialogs: DialogService, private cd: ChangeDetectorRef, private elementRef: ElementRef, - private zone: NgZone) { + private zone: NgZone, + private sanitizer: DomSanitizer) { super(); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } const title = this.translate.instant('widget-config.delete-action-title'); const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); - this.dialogs.confirm(title, content, + const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content); + this.dialogs.confirm(title, safeContent, this.translate.instant('action.no'), this.translate.instant('action.yes'), true).subscribe( (res) => { From 1984615d5ac1a2fee2801b45275bd9e91704ebf9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 24 Sep 2025 11:45:18 +0300 Subject: [PATCH 5/7] rolled back @Noxss for body that can contain html --- .../template/DeliveryMethodNotificationTemplate.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index d9b9df0fdf..e660d49bca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -24,7 +24,6 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -import org.thingsboard.server.common.data.validation.NoXss; import java.util.List; @@ -44,7 +43,6 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty - @NoXss protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { From 95ed1408f3c8cf38e09ecb6e9ed47eac0c09629b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 17 Sep 2025 12:25:23 +0300 Subject: [PATCH 6/7] fixed XSS vulnerabilities in the Rule node --- .../src/app/core/interceptors/global-http-interceptor.ts | 4 +++- ui-ngx/src/app/core/utils.ts | 7 ++++++- .../home/pages/rulechain/rule-node-details.component.ts | 1 + .../home/pages/rulechain/rulechain-page.component.ts | 9 ++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 12d3c78817..dccda985b4 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { parseHttpErrorMessage } from '@core/utils'; import { getInterceptorConfig } from './interceptor.util'; +import { DomSanitizer } from '@angular/platform-browser'; const tmpHeaders = {}; @@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { private dialogService: DialogService, private translate: TranslateService, private authService: AuthService, + private sanitizer: DomSanitizer ) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { @@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } if (unhandled && !ignoreErrors) { - const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType); + const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer); this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout); } return throwError(() => errorResponse); diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 0fc948ac96..8ef3dc074e 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -31,6 +31,8 @@ import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { SecurityContext } from '@angular/core'; const varsRegex = /\${([^}]*)}/g; @@ -809,7 +811,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str } export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, - translate: TranslateService, responseType?: string): {message: string; timeout: number} { + translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} { let error = null; let errorMessage: string; let timeout = 0; @@ -837,6 +839,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText; errorMessage = errorText; } + if(sanitizer) { + errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage); + } return {message: errorMessage, timeout}; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 05f7261c44..3f52cc0f61 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -22,6 +22,7 @@ import { OnDestroy, OnInit, Output, + SecurityContext, SimpleChanges, ViewChild } from '@angular/core'; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2ac9b806e6..f7ead66646 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -26,6 +26,7 @@ import { OnInit, QueryList, Renderer2, + SecurityContext, SkipSelf, ViewChild, ViewChildren, @@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-rulechain-page', @@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private changeDetector: ChangeDetectorRef, + private sanitizer:DomSanitizer, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder) { @@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent name = node.name; desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name; if (node.additionalInfo) { - details = node.additionalInfo.description; + details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description); } } + + name = this.sanitizer.sanitize(SecurityContext.HTML, name); + desc = this.sanitizer.sanitize(SecurityContext.HTML, desc); + let tooltipContent = '
' + '
' + '
' + name + '
' + From 986d6289b9e57d4c224d609c5776585380d3d94e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 24 Sep 2025 13:16:04 +0300 Subject: [PATCH 7/7] remove unused dependency --- .../modules/home/pages/rulechain/rule-node-details.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 3f52cc0f61..05f7261c44 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -22,7 +22,6 @@ import { OnDestroy, OnInit, Output, - SecurityContext, SimpleChanges, ViewChild } from '@angular/core';