From ed57ecca9938f93cb2fbcb468d1a471104a6be1d Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sun, 16 May 2021 13:27:58 +0530 Subject: [PATCH] Sanitize HTML strings passed to buefy.toast(). The buefy toast component does not sanitize HTML leaving it open to XSS. This patch centralised all toast calls in the app to a util function which sanitizes HTML strings before passing to toast(). Closes #357. --- frontend/src/utils.js | 16 +++++++++++++++- frontend/src/views/Import.vue | 6 +----- frontend/src/views/ListForm.vue | 12 ++---------- frontend/src/views/Lists.vue | 6 +----- frontend/src/views/SubscriberForm.vue | 27 +++++---------------------- frontend/src/views/Subscribers.vue | 25 +++++-------------------- frontend/src/views/TemplateForm.vue | 12 ++---------- frontend/src/views/Templates.vue | 20 +++----------------- 8 files changed, 34 insertions(+), 90 deletions(-) diff --git a/frontend/src/utils.js b/frontend/src/utils.js index 646e98b..74ea913 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -9,6 +9,17 @@ dayjs.extend(relativeTime); const reEmail = /(.+?)@(.+?)/ig; +const htmlEntities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=', +}; + export default class Utils { constructor(i18n) { this.i18n = i18n; @@ -67,6 +78,9 @@ export default class Utils { return out.toFixed(2) + pfx; } + // https://stackoverflow.com/a/12034334 + escapeHTML = (html) => html.replace(/[&<>"'`=/]/g, (s) => htmlEntities[s]); + // UI shortcuts. confirm = (msg, onConfirm, onCancel) => { Dialog.confirm({ @@ -98,7 +112,7 @@ export default class Utils { toast = (msg, typ, duration) => { Toast.open({ - message: msg, + message: this.escapeHTML(msg), type: !typ ? 'is-success' : typ, queue: false, duration: duration || 2000, diff --git a/frontend/src/views/Import.vue b/frontend/src/views/Import.vue index 1c5bebb..220fefe 100644 --- a/frontend/src/views/Import.vue +++ b/frontend/src/views/Import.vue @@ -278,11 +278,7 @@ export default Vue.extend({ // Post. this.$api.importSubscribers(params).then(() => { // On file upload, show a confirmation. - this.$buefy.toast.open({ - message: this.$t('import.importStarted'), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('import.importStarted')); // Start polling status. this.pollStatus(); diff --git a/frontend/src/views/ListForm.vue b/frontend/src/views/ListForm.vue index 8799957..044de34 100644 --- a/frontend/src/views/ListForm.vue +++ b/frontend/src/views/ListForm.vue @@ -84,11 +84,7 @@ export default Vue.extend({ this.$api.createList(this.form).then((data) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.created', { name: data.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.created', { name: data.name })); }); }, @@ -96,11 +92,7 @@ export default Vue.extend({ this.$api.updateList({ id: this.data.id, ...this.form }).then((data) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.updated', { name: data.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.updated', { name: data.name })); }); }, }, diff --git a/frontend/src/views/Lists.vue b/frontend/src/views/Lists.vue index 740cd40..c57c206 100644 --- a/frontend/src/views/Lists.vue +++ b/frontend/src/views/Lists.vue @@ -181,11 +181,7 @@ export default Vue.extend({ this.$api.deleteList(list.id).then(() => { this.getLists(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.deleted', { name: list.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.deleted', { name: list.name })); }); }, ); diff --git a/frontend/src/views/SubscriberForm.vue b/frontend/src/views/SubscriberForm.vue index 5a034ed..62a3d89 100644 --- a/frontend/src/views/SubscriberForm.vue +++ b/frontend/src/views/SubscriberForm.vue @@ -119,11 +119,7 @@ export default Vue.extend({ this.$api.createSubscriber(data).then((d) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.created', { name: d.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.created', { name: d.name })); }); }, @@ -150,11 +146,7 @@ export default Vue.extend({ this.$api.updateSubscriber(data).then((d) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.updated', { name: d.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.updated', { name: d.name })); }); }, @@ -164,21 +156,12 @@ export default Vue.extend({ try { attribs = JSON.parse(str); } catch (e) { - this.$buefy.toast.open({ - message: `${this.$t('subscribers.invalidJSON')}: ${e.toString()}`, - type: 'is-danger', - duration: 3000, - queue: false, - }); + this.$utils.toast(`${this.$t('subscribers.invalidJSON')}: ${e.toString()}`, + 'is-danger', 3000); return null; } if (attribs instanceof Array) { - this.$buefy.toast.open({ - message: 'Attributes should be a map {} and not an array []', - type: 'is-danger', - duration: 3000, - queue: false, - }); + this.$utils.toast('Attributes should be a map {} and not an array []', 'is-danger', 3000); return null; } diff --git a/frontend/src/views/Subscribers.vue b/frontend/src/views/Subscribers.vue index f7a482b..cd643aa 100644 --- a/frontend/src/views/Subscribers.vue +++ b/frontend/src/views/Subscribers.vue @@ -355,11 +355,7 @@ export default Vue.extend({ this.$api.deleteSubscriber(sub.id).then(() => { this.querySubscribers(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.deleted', { name: sub.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.deleted', { name: sub.name })); }); }, ); @@ -406,11 +402,7 @@ export default Vue.extend({ .then(() => { this.querySubscribers(); - this.$buefy.toast.open({ - message: this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers })); }); }; } else { @@ -422,11 +414,8 @@ export default Vue.extend({ }).then(() => { this.querySubscribers(); - this.$buefy.toast.open({ - message: this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('subscribers.subscribersDeleted', + { num: this.numSelectedSubscribers })); }); }; } @@ -454,11 +443,7 @@ export default Vue.extend({ fn(data).then(() => { this.querySubscribers(); - this.$buefy.toast.open({ - message: this.$t('subscribers.listChangeApplied'), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('subscribers.listChangeApplied')); }); }, }, diff --git a/frontend/src/views/TemplateForm.vue b/frontend/src/views/TemplateForm.vue index a2e46a2..745098c 100644 --- a/frontend/src/views/TemplateForm.vue +++ b/frontend/src/views/TemplateForm.vue @@ -98,11 +98,7 @@ export default Vue.extend({ this.$api.createTemplate(data).then((d) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: this.$t('globals.messages.created', { name: d.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.created', { name: d.name })); }); }, @@ -116,11 +112,7 @@ export default Vue.extend({ this.$api.updateTemplate(data).then((d) => { this.$emit('finished'); this.$parent.close(); - this.$buefy.toast.open({ - message: `'${d.name}' updated`, - type: 'is-success', - queue: false, - }); + this.$utils.toast(`'${d.name}' updated`); }); }, }, diff --git a/frontend/src/views/Templates.vue b/frontend/src/views/Templates.vue index 67e759b..9004ec2 100644 --- a/frontend/src/views/Templates.vue +++ b/frontend/src/views/Templates.vue @@ -143,35 +143,21 @@ export default Vue.extend({ this.$api.createTemplate(data).then((d) => { this.$api.getTemplates(); this.$emit('finished'); - this.$buefy.toast.open({ - message: `'${d.name}' created`, - type: 'is-success', - queue: false, - }); + this.$utils.toast(`'${d.name}' created`); }); }, makeTemplateDefault(tpl) { this.$api.makeTemplateDefault(tpl.id).then(() => { this.$api.getTemplates(); - - this.$buefy.toast.open({ - message: this.$t('globals.messages.created', { name: tpl.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.created', { name: tpl.name })); }); }, deleteTemplate(tpl) { this.$api.deleteTemplate(tpl.id).then(() => { this.$api.getTemplates(); - - this.$buefy.toast.open({ - message: this.$t('globals.messages.deleted', { name: tpl.name }), - type: 'is-success', - queue: false, - }); + this.$utils.toast(this.$t('globals.messages.deleted', { name: tpl.name })); }); }, },