Why Is Rename Disallowed?

Written by Pete Corey on Jul 14, 2015.

Have you ever tried to use $rename in a collection update from the client? If so, you’ve probably noticed this error:

Access denied. Operator $rename not allowed in a restricted collection.

If we dig into Meteor’s source, we can see that $rename is (currently) the only disallowed modifier. Why is that? To put it bluntly, the Mongo $rename operator is a lot like your cousin’s kid at your last family reunion - easy to forget about, but without constant supervision it can wreak havoc.

It can be especially easy to forget about $rename operators when building collection validators that depend on validating modifier objects.

A Validator Example

Take a look at the following update validator. It’s intended to allow a user to $set the userId on a Posts object, but only if the value you’re assigning it matches their current user’s ID:

Posts.allow({
  update: function(userId, doc, fields, modifier) {
    ...
    for (var key in modifier) {
      if (modifier[key].hasOwnProperty("userId")) {
        if (key === "$set") {
          if (modifier[key].userId !== userId) {
            return false;
          }
        } else {
          return false;
        }
      }
    }
    return true;
  }
});

In its current form, this is a solid validator. We’re checking if the user is running a modifier operation on userId. If they are, we check if it’s a $set. If it’s a $set, we verify that the value they’re setting userId to matches the current user’s ID. All other operations against userId are disallowed. Great!

Would this still be secure if $rename were an allowed operator? Consider if it were and we ran the following update:

Posts.update(post._id, {
  $rename: {
    title: 'userId'
  }
});

Now our modifier isn’t directly operating on userId. It’s operating on title, which we’re assuming we have permission to modify. In this update, title will be renamed to userId, effectively dumping the value of title into the userId field.

This means that if $rename were an allowed operator and we were using this validator, users would have the power to set userId to any arbitrary value. Malicious users could inject posts into user users’ accounts. This would be a bad thing!

No Place On The Client

The $rename operator has valid uses. It can be great for updating schemas during migrations, and doing wholesale data transformations. However, these use cases are almost never carried out through the client layer. Instead, they’re done entirely on the backend.

As we’ve seen, it can be difficult to reason about the $rename operator when writing collection validators. By effectively reversing its field list when compared to all other Mongo operators, it can easily slip through the cracks in your validation and expose potentially dangerous vulnerabilities.

Rather than expose that potential risk to all Meteor applications in exchange for functionality that arguably shouldn’t exist on the client, it seems the Meteor team decided to disallow the $rename operation on client-side updates.

The Threat Persists

However, $rename operators can be used on the server. This means that vulnerabilities exposed through incomplete validation can still exist in the wild. Take a look at this method from a real-world project I was digging into recently:

editUserProfile: function (modifier) {
  var user = Meteor.user(),
      schema = Users.simpleSchema()._schema;
  _.each(modifier, function (operation) {
    _.keys(operation).forEach(function (fieldName) {
      var field = schema[fieldName];
      if (!Users.can.edit(user, field, user)) {
        throw new Meteor.Error('Not allowed!');
      }
    });
  });
  ...
}

This method loops over each field of the provided modifier and checks if it is an editable field according to the Users schema. If it’s not editable and it’s trying to be modified, an exception is thrown.

For this example, let’s assume that there is an admin field in the Users schema that is un-editable by users, and a profile.location field that is editable but optional. Normally, a direct update to admin would result in an exception, but what happens if we run the following method calls:

Meteor.call('editUserProfile', {$set: {'profile.location': 'truthy'}});
Meteor.call('editUserProfile', {$rename: {'profile.location': 'admin'}});

Bam! We’re an admin!

We’re making two calls to editUserProfile. The first is setting the optional location field on our profile to "truthy". The next is renaming the profile.location to admin, which dumps our "truthy" value into admin. Since this system makes loose checks against the truthiness of the admin field, this was all we needed to escalate our privileges. This is a very bad thing!

Sealing The Cracks

It is rarely a good idea to pass a user-provided modifier object directly into a collection query running on the server. If your system is doing this, be sure that you’re whitelisting valid modifiers, rather than blacklisting bad operators.

A declarative solution to the last example’s flaw may be to extend the Users schema with an allowedOperators field. The Users.can.edit method would be passed the current operator along with the current field and determine if the schema allows the combination.

With this solution, the profile.location field would have only had $set in its allowedOperators. An attempt to $rename profile.location into admin would have failed. Success!

Basic Auth For Hiding Your Application

Written by Pete Corey on Jul 6, 2015.

The package-based Basic Auth solution presented in this post leaves DDP endpoints exposed to unauthorized users. For more information, read my follow-up post on Bypassing Package-Based Basic Auth.

Recently I’ve been playing with techniques for sharing private Meteor applications with others. An example of this may be showing a beta version of an application to a client. That client may not want any of the application exposed to the public, including splash pages or login screens.

I’ve found that a quick solution to this problem is to use good old basic authentication.

Adding basic auth to a Meteor application is incredibly simple thanks to a handful of packages that have wrapped the basic-auth-connect npm package. I’ve whipped up a quick example using kit:basic-auth and deployed it to basic-auth.meteor.com. Use username/password for your login credentials.

Adding this basic level of protection was as simple as adding the package to my project:

meteor add kit:basic-auth

And updating my settings.json file with the credentials:

{
    "basicAuth": {
        "username": "username",
        "password": "password"
    }
}

Basic authentication isn’t seen much anymore, and it’s not a particularly useful security paradigm, especially for Meteor applictations. However, it can be incredibly useful when trying to quickly lock down a web asset, or in our case, a single-page web application.

Black Box Meteor - Shared Validators

Written by Pete Corey on Jun 29, 2015.

Discover Meteor’s recent blog series about allow and deny security has done a great job at raising awareness around the security concerns that surround collection validators. Check out their Allow & Deny: A Security Primer post for a rundown on validator security.

Isomorphic Woes

One question I’ve asked myself is where do we put these allow and deny methods within our Meteor application? Intuitively, we may think that the best place to keep them is where we’ve defined our collections - in a shared location visible to the client and server. This seems to be a very common pattern amongst Meteor developers. You can even see it in Sacha’s allow & deny challenge final example. He’s defining his allow and deny methods for the Messages collection in common.js.

So what’s the big deal? Why does this matter? Well, imagine if your allow and deny methods weren’t completely secure. Would it be a good idea to ship those methods down to the client where any malicious user could browse through them at their convenience? Probably not, but that’s exactly what’s happening.

Open the allow and deny challenge MeteorPad, and in your browser’s console run the following statement:

> Messages._validators.update.allow[1].toString();
< "function (userId, doc, fields, modifier) {
    
    // log out checks
    console.log("// All checks must return true:");
    console.log(!!userId);
    console.log(!_.contains(doc.likes, userId));
    console.log(_.keys(modifier).isEqualTo(["$addToSet", "$inc"]));
    console.log(_.keys(modifier.$addToSet).isEqualTo(["likes"]));
    console.log(_.keys(modifier.$inc).isEqualTo(["likesCount"]));
    console.log(modifier.$addToSet.likes === userId);
    console.log(modifier.$inc.likesCount === 1);
    
    var check = 
      userId &&
      !_.contains(doc.likes, userId) &&
      _.keys(modifier).isEqualTo(["$addToSet", "$inc"]) &&
      _.keys(modifier.$addToSet).isEqualTo(["likes"]) &&
      _.keys(modifier.$inc).isEqualTo(["likesCount"]) &&
      modifier.$addToSet.likes === userId &&
      modifier.$inc.likesCount === 1;
      
    return check;
  }"

You’ll notice that the entire source of the allow function is visible to the client! Take some time and explore the _validators object. You’ll notice that all allow and deny methods for the Messages collection are being passed to the client.

Server-side Solution

The correct place to keep your allow and deny methods is on the server. Peruse through the official docs and read the wording surrounding the allow and deny methods. Notice that they’re specifically marked as server functionality.

When a client calls insert, update, or remove on a collection, the collection’s allow and deny callbacks are called on the server to determine if the write should be allowed.

By keeping your methods on the server, a malicious user will not be given to opportunity to dig through them with a fine-toothed comb. They would be reduced to manual testing or fuzzing to find vulnerabilities in your collection validators.

I feel it’s important for me to mention that I’m not advocating hiding your allow and deny methods as an alternative to properly securing them. You should do your absolute best to correctly secure your validators. Moving them to the server simply gives you a small layer of protection against attackers and makes their job slightly harder. Remember, security through obscurity is not security.