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!