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.
This commit is contained in:
Kailash Nadh 2021-05-16 13:27:58 +05:30
parent cf0c8f3855
commit ed57ecca99
8 changed files with 34 additions and 90 deletions

View File

@ -9,6 +9,17 @@ dayjs.extend(relativeTime);
const reEmail = /(.+?)@(.+?)/ig;
const htmlEntities = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;',
};
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,

View File

@ -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();

View File

@ -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 }));
});
},
},

View File

@ -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 }));
});
},
);

View File

@ -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;
}

View File

@ -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'));
});
},
},

View File

@ -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`);
});
},
},

View File

@ -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 }));
});
},
},