Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 SbaAlert from '@/components/sba-alert.vue';

import { render } from '@/test-utils';

describe('SbaAlert', () => {
it('should render string error message', async () => {
render(SbaAlert, {
props: {
error: 'Something went wrong',
},
});

expect(await screen.findByText('Something went wrong')).toBeInTheDocument();
});

it('should preserve safe markup in sanitized message', async () => {
render(SbaAlert, {
props: {
error: 'Request failed: <strong>timeout</strong>',
},
});

const strong = await screen.findByText('timeout');
expect(strong.tagName).toBe('STRONG');
});

it('should remove XSS vectors from string error', async () => {
render(SbaAlert, {
props: {
error:
'<img src=x onerror="window.__xss = 1"><script>window.__xss = 1</script>',
},
});

await screen.findByRole('alert');
expect(document.querySelectorAll('img').length).toBe(0);
expect(document.querySelectorAll('script').length).toBe(0);
expect((window as any).__xss).toBeUndefined();
});

it('should remove XSS vectors from Error message', async () => {
render(SbaAlert, {
props: {
error: new Error('<img src=x onerror="window.__xss = 1">boom'),
},
});

expect(await screen.findByText('boom')).toBeInTheDocument();
expect(document.querySelectorAll('img').length).toBe(0);
expect((window as any).__xss).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { defineComponent } from 'vue';

import FontAwesomeIcon from '@/components/font-awesome-icon';

import { sanitizeHtml } from '@/utils/sanitizeHtml';

export const Severity = {
ERROR: 'ERROR',
WARN: 'WARN',
Expand Down Expand Up @@ -90,10 +92,10 @@ export default defineComponent({
computed: {
message() {
if (this.error instanceof Error) {
return this.error.message;
return sanitizeHtml(this.error.message);
}
if (typeof this.error === 'string') {
return this.error;
return sanitizeHtml(this.error);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Application from '@/services/application';
import Instance from '@/services/instance';
import { sanitizeHtml } from '@/utils/sanitizeHtml';

export interface ActionHandler {
restart(item: any): Promise<void>;
Expand All @@ -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;
Expand All @@ -28,22 +29,26 @@ 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,
}),
),
);
}
}

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;
Expand All @@ -52,22 +57,26 @@ 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,
}),
),
);
}
}

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;
Expand All @@ -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,
}),
),
);
}
}
Expand All @@ -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;
Expand All @@ -108,22 +119,26 @@ 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,
}),
),
);
}
}

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;
Expand All @@ -132,22 +147,30 @@ 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,
}),
),
);
}
}

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;
Expand All @@ -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,
}),
),
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
<p class="control has-inline-text">
<span
v-html="
t('applications.suppress_notifications_on', {
name: object.id || object.name,
})
sanitizeHtml(
t('applications.suppress_notifications_on', {
name: object.id || object.name,
}),
)
"
/>&nbsp;
<sba-select
Expand Down Expand Up @@ -53,9 +55,11 @@
<p class="control has-inline-text">
<span
v-html="
t('applications.notifications_suppressed_for', {
name: object.id || object.name,
})
sanitizeHtml(
t('applications.notifications_suppressed_for', {
name: object.id || object.name,
}),
)
"
/>&nbsp;
<strong
Expand Down Expand Up @@ -85,6 +89,8 @@
<script>
import { useI18n } from 'vue-i18n';

import { sanitizeHtml } from '@/utils/sanitizeHtml';

export default {
props: {
object: {
Expand All @@ -102,6 +108,7 @@ export default {
return {
t: i18n.t,
currentLocale: i18n.locale,
sanitizeHtml,
};
},
data() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
})} <strong>${notificationFilter.expiry.fromNow(true)}</strong>.`,
sanitizeHtml(
`${t('applications.notifications_suppressed_for', {
name:
notificationFilter.applicationName || notificationFilter.instanceId,
})} <strong>${notificationFilter.expiry.fromNow(true)}</strong>.`,
),
);
} catch (error) {
console.warn('Adding notification filter failed:', error);
Expand Down
Loading
Loading