MongoDB modifier objects are hard. Incredibly hard. When you’re dealing with almost two dozen different update operators, it’s difficult to imagine all the ways in which a piece of data can be changed.
A few months ago I found an interesting issue in Telescope that perfectly highlights this problem. Telescope’s method to complete a user’s profile wasn’t correctly validating the MongoDB modifier being passed in. Exploiting that, I was able to pass in an underhanded modifier and give myself instant admin access.
Don't mistake this post as a warning against using Telescope; the vulnerability I discuss here was immediately patched after it was reported. The Telescope project continuously impresses me with its architectural choices and its obvious focus on code quality and understandability!
Security In Telescope
Telescope is an interesting project. While I constantly talk about how you should rigorously check
all of your method and publication arguments, Telescope does did very little of this - at least at the time I discovered this vulnerability. Contrary to what I might have you believe, this didn’t cause the world to end. In fact, I had trouble finding any security issues at all in the project.
How is this possible? Surely without checking arguments, vulnerabilities abound!
Telescope achieved security through its heavy use of what Sacha Greif, Telescope’s creator, calls Query Constructors. Instead of directly passing user input into query and modifier objects, Telescope uses that user data to guide the construction of new query objects. User input is only injected into these new objects when absolutely necessary, and in those cases it’s thoroughly and explicitly sanitized.
Digging Into Validation
Despite this hardened architectural approach, there was one piece of code that caught my eye while digging through Telescope’s source. The completeUserProfile
method was taking in a modifier object from the client and, after validation, passing it directly into a call to Users.update
.
The validation process seemed straight-forward. Each field in the users
schema maintained a list of roles allowed to modify that field. The completeUserProfile
method looped over each field being modified and checked that the user had the required role:
// go over each field and throw an error if it's not editable
// loop over each operation ($set, $unset, etc.)
_.each(modifier, function (operation) {
// loop over each property being operated on
_.keys(operation).forEach(function (fieldName) {
var field = schema[fieldName];
if (!Users.can.editField(user, field, user)) {
throw new Meteor.Error("disallowed_property", ...);
}
});
});
So, users with "member"
or "admin"
roles could modify telescope.displayName
, but only users with the "admin"
role could modify isAdmin
:
displayName: {
...
editableBy: ["member", "admin"]
}
isAdmin: {
...
editableBy: ["admin"]
}
$Renaming For Fun And Profit
But $set
and $unset
aren’t the only update operators at our disposal. The validation rules described above mean that users with the "member"
role could run any update operator on displayName
.
What would happen if I $rename
displayName
to isAdmin
? Let’s try it!
Meteor.call("completeUserProfile", {
$rename: {
"telescope.displayName": "isAdmin"
}
}, Meteor.userId());
Instantly, various admin controls appear in our browser (isn’t reactivity cool?)! And just like that, we gave ourself admin permissions.
So, what’s going on here?
Let’s assume we had a value in displayName
; let’s say it was "YouBetcha"
. In that case, our user document would look something like this:
{
...
isAdmin: false,
telescope: {
...
displayName: "YouBetcha"
}
}
By running an update on our user document that renames telescope.displayName
to isAdmin
, I’m effectively dumping the value of "YouBetcha"
into isAdmin
. My user document would now look something like this:
{
...
isAdmin: "YouBetcha",
telescope: {
...
}
}
Interestingly, SimpleSchema does not enforce type constraints during $rename
, so we can happily dump our String
into the Boolean
isAdmin
field.
Most of the admin checks throughout Telescope were checks against the truthiness of isAdmin
, rather than strict checks (user.isAdmin === true
), or checks against the users’ roles, so our isAdmin
value of "YouBetcha"
gives us admin access throughout the system!
The Fix & Final Thoughts
After reporting this fix, Sacha immediately fixed this issue in the v0.21.1 release of Telescope.
His first fix was to disallow $rename
across the board, just like Meteor does in updates originating from the client. Later, he went on to check
that the modifiers being used are either $set
or $unset
.
MongoDB modifier objects can be very difficult to work with, especially in the context of security. You may be preventing $set
updates against certain fields, but are you also preventing $inc
updates, or even $bin
updates? Are you disallowing $push
, but forgetting $addToSet
? Are you appropriately handling $rename
when dealing with raw modifier objects?
All of these things need to be taken into consideration when writing collection validators, or accepting modifier object from clients in your Meteor methods. It’s often a better solution to whitelist the modifiers you expect, and disallow the rest.
Are you using a vulnerable version of Telescope? Use my Package Scan tool to find out. You can also include Package Scan as part of your build process by adding east5th:package-scan
to your Meteor project:
meteor add east5th:package-scan