Rename Your Way To Admin Rights

Written by Pete Corey on Oct 19, 2015.

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

Package Scan Community Contributions

Written by Pete Corey on Oct 13, 2015.

This past month was a good month for Package Scan.

Not only did I release the Package Scan Web Tool, but the project also saw its first community contributions! Evolross and Charles Watson both found vulnerable packages in the wild and added alerts to Package Scan.


Charles found an issue with older versions of babrahams:editable-json (<= 0.5.1) that allows for any user to run arbitrary updates on any document in any collection. This means that a user could potentially run an update on their own user document to give themselves administrator permissions:

Meteor.call("editableJSON_update", "users", Meteor.userId(), {
  $set: {
    roles: ["admin"]
  }
});

Evolross reported an ongoing issue with the current version of cfs:standard-packages. Exceptions in your transformWrite callbacks can trigger repeated server crashes as CollectionFS attempts to transform the file on each server restart. These exceptions can easily be triggered by users uploading files of unexpected types that blow up when passed into gm. Using this bug, an attacker could easily orchestrate a Denial of Service attack against your application.

Because of the ubiquity of CollectionFS (57,000 app installs on Atmosphere), and because this setup is explicitly described in the documentation, I felt this warranted a Package Scan alert.

Take a look at the Github issue and another related issue with a helpful work-around.


If you find a vulnerable Meteor package in the wild, submit an alert to help the community!

Also be sure to add east5th:package-scan to your Meteor project, or use the drag & drop web tool to stay up to date on the latest Meteor security alerts.

meteor add east5th:package-scan

Slimming Down Fat Models

Written by Pete Corey on Oct 5, 2015.

I’ll admit it; I have a weight problem. Lately, my models have been getting fat.

Really fat.

To be honest, I didn’t see this coming. I thought I’ve been doing everything right. I’ve been keeping my views and controllers nice and thin, and I’ve been working hard to encapsulate complicated business logic into appropriately named model methods.

This might be a little awkward, but let me show you… Suppose we’ve got a Job Board application. In it, there’s a Jobs collection. We’re using dburles:collection-helpers to build a simple Jobs model. Imagine that we want to allow users to close jobs they’ve created when the position is filled. Let’s create a model method that can be called from our controller layer (our Meteor method):

Jobs.helpers({
  ...
  closeJob: () => {
    Jobs.update(this._id, {
      $set: {
        closed: true
      }
    });
  }
  ...
});

Ah, but wait. When a job closes we also need to email the job owner and congratulate them on successfully filling the position. Let’s update our model.

closeJob: () => {
  Jobs.update(this._id, {
    $set: {
      closed: true
    }
  });

  Email.send({
    to: this.getOwner().getEmail(),
    body: "Congrats on filling the position!"
  });
}

After getting some user feedback, we’ve realized that we need to congratulate the person who filled the position. Also, we should notify all of the other applicants who applied, but didn’t get the job. Also, we need to do some housekeeping and update the Organization that this job belongs to.

Let’s get back to work on our model.

closeJob: () => {
  Jobs.update(this._id, {
    $set: {
      closed: true
    }
  });

  Email.send({
    to: this.getOwner().getEmail(),
    body: "Congrats on filling the position!"
  });

  Email.send({
    to: this.getFiller().getEmail(),
    body: "Congrats on landing the job!"
  });

  this.getApplicants().forEach(applicant => {
    Email.send({
      to: applicant.getEmail(),
      body: "Sorry, but the job has been closed."
    });
  });

  this.getOrganization().closeJob();
}

Whew, that looks mostly complete!

Though technically, job owners will close a job for a variety of reasons. Maybe they’ve found someone to fill the position, or maybe they’ve just decided to take the job off of the job board for other reasons. If they use the closeJob model method, the system will send the job owner a congratulations email, and all applicants emails about the job being filled. That’s no good.

Maybe the solution is to pass in flags to determine whether we should kick off various emails:

closeJob: (congratulateOwner, congratulateFiller, notifyApplicants) => {
  Jobs.update(this._id, {
    $set: {
      closed: true
    }
  });

  if (congratulateOwner) {
    Email.send({
      to: this.getOwner().getEmail(),
      body: "Congrats on filling the position!"
    });
  }

  if (congratulateFiller) {
    Email.send({
      to: this.getFiller().getEmail(),
      body: "Congrats on landing the job!"
    });
  }

  if (notifyApplicants) {
    this.getApplicants().forEach(applicant => {
      Email.send({
        to: applicant.getEmail(),
        body: "Sorry, but the job has been closed."
      });
    });
  }

  this.getOrganization().closeJob();
}

The complexity of our model method is getting out of hand! The controller calling this method is forced to know much more about the internals of the method than it should. The bottom line is that our model is getting too fat.


So how do we shed all of this weight that’s built up on our model? We can find inspiration in more complicated system architectures and design philosophies such as Command Busses, Event Sourcing and even Domain Driven Design. All of these design patterns and philosophies make fundamental use of server-side events to segregate and organize functionality within large applications. By leveraging events in our application, we can make huge gains in terms of code cleanliness, understandability, and testability.

Let’s take a step back and think about our domain in reverse. We have a few things we want to do on every job closure. Whenever a job is closed because a suitable applicant was found, we want to send an email congratulating the job owner:

when("JobWasFilled", job => {
  Email.send({
    to: job.getOwner().getEmail(),
    body: "Congrats on filling the position!"
  });
});

We’ll also want to congratulate the user who filled the job:

when("JobWasFilled", job => {
  Email.send({
    to: job.getFiller().getEmail(),
    body: "Congrats on landing the job!"
  });
});

Any time a job posting is closed for any reason, we’ll want to notify all of our users who applied for that job:

when("JobWasClosed", job => {
  job.getApplicants().forEach(applicant => {
    Email.send({
      to: applicant.getEmail(),
      body: "Sorry, but the job has been closed."
    });
  });
});

And we also want to always update the job’s parent organization:

when("JobWasClosed", job => {
  job.getOrganization().closeJob();
});

All of these pieces of code can live in their own files on the server. We can organize them in such a way that makes immediate sense to anyone looking at the structure of our project:

server/
  listeners/
    jobWasClosed/
      closeJobOnOrganization.js
      notifyOtherApplicants.js
    jobWasFilled/
      congratulateOwner.js
      congratulateFiller.js

Now, our closeJob model method is incredibly simple, straight forward, and best of all, only has to worry about the model:

closeJob: () => {
  Jobs.update(this._id, {
    $set: {
      closed: true
    }
  });

  EventEmitter.emit("JobWasClosed", this);
}

We’ll also need to emit the JobWasFilled event from our method layer:

Meteor.methods({
  fillJob: jobId => {
    ...
    job.closeJob();
    EventEmitter.emit("JobWasFilled", job);
  }
});

Now, when a job is filled, the JobWasClosed event will be fired by the model, and the JobWasFilled event will be fired by our method layer. Our listeners will pick up on these events and do their duty. If closeJob is called through some other means, our JobWasClosed listeners will be triggered, but the JobWasFilled listeners will not. Perfect!

I feel so thin!


Server-side events are an architectural gem that can be easily incorporated into most applications to provide clean, easy decoupling of components. Events let you cleanly separate functionality, organize it into easy to understand chunks, better reason about the workings of your application, and more effectively write tests.

If you decide to start using events in your applications, I highly recommend spending a few dollars and watching this excellent Laracasts series on Commands and Domain Events. It’s targeted towards PHP developers using the Laravel framework, but the ideas are completely applicable to any software project. Jeffrey Way is an excellent teacher and masterfully presents the ideas and benefits behind server-side events.