In the past, I’ve talked about tracking down Cross Site Scripting (XSS) vulnerabilities within your Meteor application by hunting for triple-brace injections. I argued that XSS was relatively uncommon because you needed to explicitly use this special injection syntax to inject raw HTML into the DOM.

While this is mostly true, there are other ways for XSS to rear its ugly head in your Meteor application.


Imagine the following situation. You have a template that uses data from a MongoDB collection to populate a dropdown. To render the dropdown, you’re using a jQuery plugin. This plugin expects you to provide the dropdown options as an argument, rather than through the DOM:

Template.choices.onRendered(function() {

  // Build our dropdown options from the Choices collection
  let options = Choices.find().fetch().map(choice => {
    return {
      label: choice.name,
      value: choice._id_
    };
  });
  
  // Initialize the dropdown
  this.$("select").dropdown({
    options: options
  });
});

If you took the time to look at how the jQuery plugin works, you would notice that it’s taking the options we’re providing it, and dumping them directly into the DOM:

$(...)
  .$("<option>")
  .attr("value", option.value)
  .html(option.label);

This plugin is making no attempt to sanitize the values or labels that are being injected into the DOM. Its operating under the assumption that the data will already be sanitized by the time it reaches the plugin.


Unfortunately, this opens the door to a Stored Cross Site Scripting vulnerability in our application. Because neither the jQuery plugin nor our application are sanitizing the values pulled from the database before injecting them into the DOM, a malicious user could easily take advantage of the situation.

Imagine that an attacker were to change the name of one of the Choice documents to a string containing some malicious <script> tag:

Choices.update(..., {
  $set: {
    name: `<script>
             Roles.addUsersToRoles("${Meteor.userId()}", "admin");
          </script>`
  }
});

Now, whenever that option is rendered in the dropdown, that malicious Javascript will be executed.

Interestingly, if another user were to use the dropdown, the malicious Javascript would run on their behalf. This means that if an Administrator were to open the dropdown and render this malicious option, they would unintentionally give the admin role to the attacking user.

This is a bad thing.


Thankfully, the solution to this issue is relatively straight-forward. Before passing our options into the jQuery plugin, we should sanitize them to prevent malicious tags from being inserted into the DOM as HTML.

The Blaze package comes with a handy utility called Blaze._escape that does just that. It takes in any string and escapes any HTML special characters into their corresponding HTML encoded form.

We can incorporate Blaze._escape into our previous example like so:

Template.choices.onRendered(function() {
  let options = Choices.find().fetch().map(choice => {
    return {
      label: Blaze._escape(choice.name),
      value: choice._id_
    };
  });
  this.$("select").dropdown({
    options: options
  });
});

This would transform the malicious name into a benign string that could safely be injected into the DOM:

'&lt;script&gt;Roles.addUsersToRoles("...", "admin");&lt;/script&gt;'

When injected into the DOM, this would be interpreted as plain text, rather than a <script> tag. This means that the malicious Javascript would not be executed, and the Cross Site Scripting vulnerability would no longer exist!


It’s important to always take responsibility for the safety of your application. When using third-party plugins or packages, never make assumptions about what they are, or are not doing - especially when it comes to security.

When in doubt, dig into the source and find out exactly what’s going on!