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 reEmail = /(.+?)@(.+?)/ig;
const htmlEntities = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;',
};
export default class Utils { export default class Utils {
constructor(i18n) { constructor(i18n) {
this.i18n = i18n; this.i18n = i18n;
@ -67,6 +78,9 @@ export default class Utils {
return out.toFixed(2) + pfx; return out.toFixed(2) + pfx;
} }
// https://stackoverflow.com/a/12034334
escapeHTML = (html) => html.replace(/[&<>"'`=/]/g, (s) => htmlEntities[s]);
// UI shortcuts. // UI shortcuts.
confirm = (msg, onConfirm, onCancel) => { confirm = (msg, onConfirm, onCancel) => {
Dialog.confirm({ Dialog.confirm({
@ -98,7 +112,7 @@ export default class Utils {
toast = (msg, typ, duration) => { toast = (msg, typ, duration) => {
Toast.open({ Toast.open({
message: msg, message: this.escapeHTML(msg),
type: !typ ? 'is-success' : typ, type: !typ ? 'is-success' : typ,
queue: false, queue: false,
duration: duration || 2000, duration: duration || 2000,

View File

@ -278,11 +278,7 @@ export default Vue.extend({
// Post. // Post.
this.$api.importSubscribers(params).then(() => { this.$api.importSubscribers(params).then(() => {
// On file upload, show a confirmation. // On file upload, show a confirmation.
this.$buefy.toast.open({ this.$utils.toast(this.$t('import.importStarted'));
message: this.$t('import.importStarted'),
type: 'is-success',
queue: false,
});
// Start polling status. // Start polling status.
this.pollStatus(); this.pollStatus();

View File

@ -84,11 +84,7 @@ export default Vue.extend({
this.$api.createList(this.form).then((data) => { this.$api.createList(this.form).then((data) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.created', { name: data.name }));
message: this.$t('globals.messages.created', { name: data.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
@ -96,11 +92,7 @@ export default Vue.extend({
this.$api.updateList({ id: this.data.id, ...this.form }).then((data) => { this.$api.updateList({ id: this.data.id, ...this.form }).then((data) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.updated', { name: data.name }));
message: this.$t('globals.messages.updated', { name: data.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
}, },

View File

@ -181,11 +181,7 @@ export default Vue.extend({
this.$api.deleteList(list.id).then(() => { this.$api.deleteList(list.id).then(() => {
this.getLists(); this.getLists();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.deleted', { name: list.name }));
message: this.$t('globals.messages.deleted', { name: list.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
); );

View File

@ -119,11 +119,7 @@ export default Vue.extend({
this.$api.createSubscriber(data).then((d) => { this.$api.createSubscriber(data).then((d) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.created', { name: d.name }));
message: this.$t('globals.messages.created', { name: d.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
@ -150,11 +146,7 @@ export default Vue.extend({
this.$api.updateSubscriber(data).then((d) => { this.$api.updateSubscriber(data).then((d) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.updated', { name: d.name }));
message: this.$t('globals.messages.updated', { name: d.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
@ -164,21 +156,12 @@ export default Vue.extend({
try { try {
attribs = JSON.parse(str); attribs = JSON.parse(str);
} catch (e) { } catch (e) {
this.$buefy.toast.open({ this.$utils.toast(`${this.$t('subscribers.invalidJSON')}: ${e.toString()}`,
message: `${this.$t('subscribers.invalidJSON')}: ${e.toString()}`, 'is-danger', 3000);
type: 'is-danger',
duration: 3000,
queue: false,
});
return null; return null;
} }
if (attribs instanceof Array) { if (attribs instanceof Array) {
this.$buefy.toast.open({ this.$utils.toast('Attributes should be a map {} and not an array []', 'is-danger', 3000);
message: 'Attributes should be a map {} and not an array []',
type: 'is-danger',
duration: 3000,
queue: false,
});
return null; return null;
} }

View File

@ -355,11 +355,7 @@ export default Vue.extend({
this.$api.deleteSubscriber(sub.id).then(() => { this.$api.deleteSubscriber(sub.id).then(() => {
this.querySubscribers(); this.querySubscribers();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.deleted', { name: sub.name }));
message: this.$t('globals.messages.deleted', { name: sub.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
); );
@ -406,11 +402,7 @@ export default Vue.extend({
.then(() => { .then(() => {
this.querySubscribers(); this.querySubscribers();
this.$buefy.toast.open({ this.$utils.toast(this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers }));
message: this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers }),
type: 'is-success',
queue: false,
});
}); });
}; };
} else { } else {
@ -422,11 +414,8 @@ export default Vue.extend({
}).then(() => { }).then(() => {
this.querySubscribers(); this.querySubscribers();
this.$buefy.toast.open({ this.$utils.toast(this.$t('subscribers.subscribersDeleted',
message: this.$t('subscribers.subscribersDeleted', { num: this.numSelectedSubscribers }), { num: this.numSelectedSubscribers }));
type: 'is-success',
queue: false,
});
}); });
}; };
} }
@ -454,11 +443,7 @@ export default Vue.extend({
fn(data).then(() => { fn(data).then(() => {
this.querySubscribers(); this.querySubscribers();
this.$buefy.toast.open({ this.$utils.toast(this.$t('subscribers.listChangeApplied'));
message: this.$t('subscribers.listChangeApplied'),
type: 'is-success',
queue: false,
});
}); });
}, },
}, },

View File

@ -98,11 +98,7 @@ export default Vue.extend({
this.$api.createTemplate(data).then((d) => { this.$api.createTemplate(data).then((d) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(this.$t('globals.messages.created', { name: d.name }));
message: this.$t('globals.messages.created', { name: d.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
@ -116,11 +112,7 @@ export default Vue.extend({
this.$api.updateTemplate(data).then((d) => { this.$api.updateTemplate(data).then((d) => {
this.$emit('finished'); this.$emit('finished');
this.$parent.close(); this.$parent.close();
this.$buefy.toast.open({ this.$utils.toast(`'${d.name}' updated`);
message: `'${d.name}' updated`,
type: 'is-success',
queue: false,
});
}); });
}, },
}, },

View File

@ -143,35 +143,21 @@ export default Vue.extend({
this.$api.createTemplate(data).then((d) => { this.$api.createTemplate(data).then((d) => {
this.$api.getTemplates(); this.$api.getTemplates();
this.$emit('finished'); this.$emit('finished');
this.$buefy.toast.open({ this.$utils.toast(`'${d.name}' created`);
message: `'${d.name}' created`,
type: 'is-success',
queue: false,
});
}); });
}, },
makeTemplateDefault(tpl) { makeTemplateDefault(tpl) {
this.$api.makeTemplateDefault(tpl.id).then(() => { this.$api.makeTemplateDefault(tpl.id).then(() => {
this.$api.getTemplates(); this.$api.getTemplates();
this.$utils.toast(this.$t('globals.messages.created', { name: tpl.name }));
this.$buefy.toast.open({
message: this.$t('globals.messages.created', { name: tpl.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
deleteTemplate(tpl) { deleteTemplate(tpl) {
this.$api.deleteTemplate(tpl.id).then(() => { this.$api.deleteTemplate(tpl.id).then(() => {
this.$api.getTemplates(); this.$api.getTemplates();
this.$utils.toast(this.$t('globals.messages.deleted', { name: tpl.name }));
this.$buefy.toast.open({
message: this.$t('globals.messages.deleted', { name: tpl.name }),
type: 'is-success',
queue: false,
});
}); });
}, },
}, },