From 64b709f3779046ff545f4465652e3113e57ec97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Ko=CC=88ninger?= Date: Tue, 22 Sep 2026 15:42:16 +0200 Subject: [PATCH 1/5] fix(health-details): sanitize HTML in string-valued health details String-valued health/info detail values from a monitored instance's actuator response were inserted via v-html without sanitization, while the sibling object-valued path already sanitized its output. This allowed a malicious or compromised instance to store markup that executes as script in an administrator's browser session (stored XSS, CWE-79). Apply the same sanitizeHtml() call used by sba-formatted-obj.vue before autolinking the value. --- .../instances/details/health-details.spec.ts | 31 +++++++++++++++++++ .../instances/details/health-details.vue | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts index eff6ae53575..02093a781e2 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/health-details.spec.ts @@ -513,5 +513,36 @@ describe('HealthDetails', () => { const toggleButton = screen.queryByRole('button'); expect(toggleButton).not.toBeInTheDocument(); }); + + it('should not render HTML/script markup contained in a string detail value', async () => { + const maliciousValue = + ''; + + const healthMock = { + status: 'UP', + details: { + canary: maliciousValue, + }, + }; + + render(HealthDetails, { + props: { + name: 'db', + health: healthMock, + instance: mockInstance, + }, + }); + + const canaryDetail = await screen.findByRole('definition', { + name: 'canary', + }); + + // sanitize-html strips disallowed tags (e.g. ) entirely, so no + // markup and no onerror handler must reach the DOM. + expect(canaryDetail.innerHTML).not.toContain(' @@ -124,6 +124,7 @@ import SbaFormattedObj from '@/components/sba-formatted-obj.vue'; import Instance from '@/services/instance'; import autolink from '@/utils/autolink'; +import { sanitizeHtml } from '@/utils/sanitizeHtml'; const { t } = useI18n(); const id = useId(); From 0a14a04f28ca1c540741d57fde38a74a32167672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Ko=CC=88ninger?= Date: Tue, 22 Sep 2026 15:42:29 +0200 Subject: [PATCH 2/5] fix(ui): remove remaining unsanitized v-html sinks fed by remote data A follow-up scan of every v-html usage in spring-boot-admin-server-ui found three more sinks rendering untrusted, remote-instance-controlled data without sanitization or escaping: - env/refresh.vue: property names from an instance's /actuator/refresh response were concatenated into a raw HTML string and rendered via v-html. Replaced with a v-for/v-text list so Vue escapes each entry. - NotificationFilterSettings.vue: the registered application/instance name (attacker-controlled at registration time) was interpolated into an i18n string containing literal markup and rendered via v-html; vue-i18n does not escape interpolated params. Switched to with a scoped slot so the name is rendered as text. - m-bean-operation.vue: a JMX MBean operation name (controlled by the monitored JVM, e.g. a compromised instance) was interpolated the same way. Fixed identically with . Also switched sba-alert.vue's error message from v-html to v-text as defense in depth, since Jolokia-derived error strings can originate from a monitored instance and no legitimate use case needs HTML rendering there. Added regression tests asserting injected markup/scripts are never parsed as HTML for the health-details, m-bean-operation, and NotificationFilterSettings components. --- .../main/frontend/components/sba-alert.vue | 2 +- .../NotificationFilterSettings.spec.ts | 59 +++++++++++++++++ .../NotificationFilterSettings.vue | 29 +++++---- .../frontend/views/applications/i18n.de.json | 20 +++--- .../frontend/views/applications/i18n.en.json | 26 ++++---- .../frontend/views/applications/i18n.es.json | 14 ++-- .../frontend/views/applications/i18n.fr.json | 14 ++-- .../frontend/views/applications/i18n.is.json | 14 ++-- .../frontend/views/applications/i18n.ko.json | 26 ++++---- .../views/applications/i18n.pt-BR.json | 14 ++-- .../frontend/views/applications/i18n.ru.json | 26 ++++---- .../views/applications/i18n.zh-CN.json | 14 ++-- .../views/applications/i18n.zh-TW.json | 26 ++++---- .../frontend/views/instances/env/refresh.vue | 64 +++++++++---------- .../views/instances/jolokia/i18n.de.json | 2 +- .../views/instances/jolokia/i18n.en.json | 2 +- .../views/instances/jolokia/i18n.ru.json | 2 +- .../views/instances/jolokia/i18n.zh-TW.json | 2 +- .../jolokia/m-bean-operation.spec.ts | 19 ++++++ .../instances/jolokia/m-bean-operation.vue | 10 +-- 20 files changed, 232 insertions(+), 153 deletions(-) create mode 100644 spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.spec.ts diff --git a/spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.vue b/spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.vue index 1931df93965..20dda7278c9 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/components/sba-alert.vue @@ -27,7 +27,7 @@

-

+

diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.spec.ts b/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.spec.ts new file mode 100644 index 00000000000..4c100c2bae0 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.spec.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2014-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { screen } from '@testing-library/vue'; +import { describe, expect, it } from 'vitest'; + +import { render } from '@/test-utils'; +import NotificationFilterSettings from '@/views/applications/NotificationFilterSettings.vue'; + +describe('NotificationFilterSettings', () => { + it('does not render HTML/script markup contained in the object name', async () => { + const maliciousName = ''; + + render(NotificationFilterSettings, { + props: { + object: { name: maliciousName }, + notificationFilters: [], + }, + }); + + // The malicious markup must show up as plain text, never be parsed as HTML. + expect(await screen.findByText(maliciousName)).toBeInTheDocument(); + + // No element must have been created from the payload. + expect(document.querySelectorAll('img').length).toBe(0); + expect((window as any).__xss).toBeUndefined(); + }); + + it('renders the suppressed-for label with the instance/application id as plain text', async () => { + const maliciousId = 'bold'; + + render(NotificationFilterSettings, { + props: { + object: { id: maliciousId }, + notificationFilters: [ + { + affects: () => true, + expiry: null, + }, + ], + }, + }); + + expect(await screen.findByText(maliciousId)).toBeInTheDocument(); + expect(document.querySelectorAll('b').length).toBe(0); + }); +}); diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.vue b/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.vue index 0f7af2f719f..f577b6a1bda 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/views/applications/NotificationFilterSettings.vue @@ -19,13 +19,12 @@