Browse Source

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.
Kailash Nadh 4 years ago
parent
commit
ed57ecca99

+ 15 - 1
frontend/src/utils.js

@@ -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,

+ 1 - 5
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();

+ 2 - 10
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 }));
       });
     },
   },

+ 1 - 5
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 }));
           });
         },
       );

+ 5 - 22
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;
       }
 

+ 5 - 20
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'));
       });
     },
   },

+ 2 - 10
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`);
       });
     },
   },

+ 3 - 17
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 }));
       });
     },
   },