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!