;
@@ -19,7 +20,7 @@ export class InstanceActionHandler implements ActionHandler {
async unregister(item: Instance) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.unregister'),
- this.t('instances.unregister', { name: item.id }),
+ sanitizeHtml(this.t('instances.unregister', { name: item.id })),
);
if (!isConfirmed) {
return;
@@ -28,14 +29,18 @@ export class InstanceActionHandler implements ActionHandler {
try {
await item.unregister();
this.notificationCenter.success(
- this.t('instances.unregister_successful', { name: item.id }),
+ sanitizeHtml(
+ this.t('instances.unregister_successful', { name: item.id }),
+ ),
);
} catch (error) {
this.notificationCenter.error(
- this.t('instances.unregister_failed', {
- name: item.id || item.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('instances.unregister_failed', {
+ name: item.id || item.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
@@ -43,7 +48,7 @@ export class InstanceActionHandler implements ActionHandler {
async shutdown(item: Instance) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.shutdown'),
- this.t('instances.shutdown', { name: item.id }),
+ sanitizeHtml(this.t('instances.shutdown', { name: item.id })),
);
if (!isConfirmed) {
return;
@@ -52,14 +57,18 @@ export class InstanceActionHandler implements ActionHandler {
try {
await item.shutdown();
this.notificationCenter.success(
- this.t('instances.shutdown_successful', { name: item.id }),
+ sanitizeHtml(
+ this.t('instances.shutdown_successful', { name: item.id }),
+ ),
);
} catch (error) {
this.notificationCenter.error(
- this.t('instances.shutdown_failed', {
- name: item.id || item.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('instances.shutdown_failed', {
+ name: item.id || item.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
@@ -67,7 +76,7 @@ export class InstanceActionHandler implements ActionHandler {
async restart(item: Instance) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.restart'),
- this.t('instances.restart', { name: item.id }),
+ sanitizeHtml(this.t('instances.restart', { name: item.id })),
);
if (!isConfirmed) {
return;
@@ -76,14 +85,16 @@ export class InstanceActionHandler implements ActionHandler {
try {
await item.restart();
this.notificationCenter.success(
- this.t('instances.restarted', { name: item.id }),
+ sanitizeHtml(this.t('instances.restarted', { name: item.id })),
);
} catch (error) {
this.notificationCenter.error(
- this.t('instances.restart_failed', {
- name: item.id || item.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('instances.restart_failed', {
+ name: item.id || item.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
@@ -99,7 +110,7 @@ export class ApplicationActionHandler implements ActionHandler {
async restart(application: Application) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.restart'),
- this.t('applications.restart', { name: application.name }),
+ sanitizeHtml(this.t('applications.restart', { name: application.name })),
);
if (!isConfirmed) {
return;
@@ -108,14 +119,18 @@ export class ApplicationActionHandler implements ActionHandler {
try {
await application.restart();
this.notificationCenter.success(
- this.t('applications.restarted', { name: application.name }),
+ sanitizeHtml(
+ this.t('applications.restarted', { name: application.name }),
+ ),
);
} catch (error) {
this.notificationCenter.error(
- this.t('applications.restart_failed', {
- name: application.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('applications.restart_failed', {
+ name: application.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
@@ -123,7 +138,7 @@ export class ApplicationActionHandler implements ActionHandler {
async shutdown(application: Application) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.shutdown'),
- this.t('applications.shutdown', { name: application.name }),
+ sanitizeHtml(this.t('applications.shutdown', { name: application.name })),
);
if (!isConfirmed) {
return;
@@ -132,14 +147,20 @@ export class ApplicationActionHandler implements ActionHandler {
try {
await application.shutdown();
this.notificationCenter.success(
- this.t('applications.shutdown_successful', { name: application.name }),
+ sanitizeHtml(
+ this.t('applications.shutdown_successful', {
+ name: application.name,
+ }),
+ ),
);
} catch (error) {
this.notificationCenter.error(
- this.t('applications.shutdown_failed', {
- name: application.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('applications.shutdown_failed', {
+ name: application.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
@@ -147,7 +168,9 @@ export class ApplicationActionHandler implements ActionHandler {
async unregister(application: Application) {
const isConfirmed = await this.$sbaModal.confirm(
this.t('applications.actions.unregister'),
- this.t('applications.unregister', { name: application.name }),
+ sanitizeHtml(
+ this.t('applications.unregister', { name: application.name }),
+ ),
);
if (!isConfirmed) {
return;
@@ -156,16 +179,20 @@ export class ApplicationActionHandler implements ActionHandler {
try {
await application.unregister();
this.notificationCenter.success(
- this.t('applications.unregister_successful', {
- name: application.name,
- }),
+ sanitizeHtml(
+ this.t('applications.unregister_successful', {
+ name: application.name,
+ }),
+ ),
);
} catch (error) {
this.notificationCenter.error(
- this.t('applications.unregister_failed', {
- name: application.name,
- error: error.response.status,
- }),
+ sanitizeHtml(
+ this.t('applications.unregister_failed', {
+ name: application.name,
+ error: error.response.status,
+ }),
+ ),
);
}
}
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 745599cd8a7..948194eaa49 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
@@ -21,9 +21,11 @@
import { useI18n } from 'vue-i18n';
+import { sanitizeHtml } from '@/utils/sanitizeHtml';
+
export default {
props: {
object: {
@@ -102,6 +108,7 @@ export default {
return {
t: i18n.t,
currentLocale: i18n.locale,
+ sanitizeHtml,
};
},
data() {
diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue b/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue
index 0be53a52258..e2a70f796fa 100644
--- a/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue
+++ b/spring-boot-admin-server-ui/src/main/frontend/views/applications/index.vue
@@ -203,6 +203,7 @@ import NotificationFilter from '@/services/notification-filter';
import axios from '@/utils/axios';
import { anyValueMatches } from '@/utils/collections';
import { Subject, concatMap, mergeWith, timer } from '@/utils/rxjs';
+import { sanitizeHtml } from '@/utils/sanitizeHtml';
import { useRouterState } from '@/utils/useRouterState';
import { useSubscription } from '@/utils/useSubscription';
import ApplicationListItemAction from '@/views/applications/ApplicationListItemAction.vue';
@@ -426,10 +427,12 @@ async function addFilter({ object, ttl }) {
let notificationFilter = response.data;
notificationFilterSubject.next(notificationFilter);
notificationCenter.success(
- `${t('applications.notifications_suppressed_for', {
- name:
- notificationFilter.applicationName || notificationFilter.instanceId,
- })} ${notificationFilter.expiry.fromNow(true)}.`,
+ sanitizeHtml(
+ `${t('applications.notifications_suppressed_for', {
+ name:
+ notificationFilter.applicationName || notificationFilter.instanceId,
+ })} ${notificationFilter.expiry.fromNow(true)}.`,
+ ),
);
} catch (error) {
console.warn('Adding notification filter failed:', error);
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 9b37ddd4b61..43177b6cb2b 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
@@ -184,4 +184,30 @@ describe('HealthDetails', () => {
},
);
});
+
+ describe('XSS in string-valued details', () => {
+ it('should not render HTML/script markup contained in a string detail value', async () => {
+ render(HealthDetails, {
+ props: {
+ name: 'db',
+ health: {
+ status: 'UP',
+ details: {
+ canary:
+ '
',
+ },
+ },
+ },
+ });
+
+ const canaryDetail = await screen.findByRole('definition', {
+ name: 'canary',
+ });
+
+ expect(canaryDetail.innerHTML).not.toContain('
@@ -79,6 +79,7 @@ import { computed, useId } from 'vue';
import SbaFormattedObj from '@/components/sba-formatted-obj.vue';
import autolink from '@/utils/autolink';
+import { sanitizeHtml } from '@/utils/sanitizeHtml';
const id = useId();
diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.vue b/spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.vue
index 5e5f82d32a1..0b8920308ec 100644
--- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.vue
+++ b/spring-boot-admin-server-ui/src/main/frontend/views/instances/env/refresh.vue
@@ -21,8 +21,29 @@
-
-
+
+
+