Written by Pete Corey on May 5, 2015.

I was recently poking through the Meteor publications being used in a client project and I found an interesting vulnerability. Imagine an admin panel that shows a list of all users in the system. That page/route needs to subscribe to a publication that publishes all of the users, but only if the current user is an admin. We don’t want non-administrators having access to all of the user data in the system! Are you imagining? Good! Here’s the publication, as seen in the wild:

Meteor.publish('users', function(userId){
    if(Roles.userIsInRole(userId, 'admin')){
        return Meteor.users.find({}, {fields: {...});

This publication takes an argument that is intended to be the current user’s ID. It would be subscribed to on the client like this:

Meteor.subscribe('users', Meteor.userId());

If you’re an astute observer, you may notice a few potential problems here. Let’s dig into them!

Guess the Admin ID

Since the userId is a user provided argument, and we’re not actually validating that the currently logged in user is the user associated with the provided ID, a malicious user could potentially just guess an administrator’s ID. Or, instead of guessing the ID, they may find it in other public data (posts, comments, profiles, etc…). They could easily subscribe to the publication right from their browser console:

Meteor.subscribe('users', '[spoofed admin ID]');

But that’s assuming that they know the publication exists, right? If the malicious user can never get to the admin route, they’ll never see the subscribe happen, and they’ll have no way of knowing that they can subscribe to it. Right? Wrong!

A quick search through the minified and concatenated JavaScript served to each client will show each subscription being made (search for ".subscribe("), even if it is happening behind some kind of protection mechanism. If any client can get to it, all clients can get to it.

BYO User Object

Take a look at lines 307 and 330 of this file in the alanning:meteor-roles package. You’ll notice that isUserInRole accepts either a user ID as a string, or the entire user object. Looking deeper, we can see that if a user object is passed in, it will return true if the passed in role exists in the roles field on the user object.

So what if a malicious user subscribes to the users publication with the following userId parameter:

Meteor.subscribe('users', {roles: ['admin']});

Uh oh. We’re passing an object to our subscription, which we’re passing directly into Roles.userIsInRole. userIsInRole happily accepts this object, assuming that it’s a user object pulled from the database, and confirms for us that 'admin' is indeed in the roles field of the object. Great!

Fixing It

The correct fix for this issue is to not pass in the ID of the user, but instead use this.userId within the server method. This ensures that the user can’t “spoof” the system into thinking they’re someone else.

There are other lessons to be learned here, too.

Always check your arguments! When accepting user provided arguments in methods or publication, always use Meteor’s check method to ensure that the argument you’re getting is of the type you expect.

Lastly, it’s very important to always be aware of what’s going on in any third party code you’re using. Without thoroughly reading the docs, it might not be immediately obvious that the userIsInRole method accepts either a String or an Object. Or, maybe it’s assumed that the package itself is checking its arguments. Never assume! Always check!

Finishing up, the correct publication looks like this:

Meteor.publish('users', function(){
    if(Roles.userIsInRole(this.userId, 'admin')){
        return Meteor.users.find({}, {fields: {...});