Meteor Security in the Wild

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: {...});
    }
});

Meteor Package Scan

Written by Pete Corey on Apr 27, 2015.

In response to my last Meteor Black Box post about Package Scanning, I was inspired to build a tool to help improve the safety of the Meteor package ecosystem. That tool is Package Scan!

Package Scan is a Meteor package that will parse your .meteor/versions file and compare the packages being used by your project against a list of packages with known security issues. If a vulnerable package is detected, a warning will be shown in your server logs. Package Scan is debug only, so it will never be built into your production application.

The goal is Package Scan is to give Meteor developers an extra layer of knowledge and insight about the packages they’re using in their projects. My vision is that, with help from the community, Package Scan will enable developers to quickly discover and understand the security implications of the packages being used in their projects, or give some level of peace of mind if no vulnerable packages are detected.

The key to Package Scan will be community involvement. Without help from other package developers and users, I’ll have no hope of ever maintaining a comprehensive and up to date package warning list. It’s my hope that whenever a security problem is discovered in a version of a package, Package Scan will be updated with a new alert.

The mechanism I’m using for updating the Package Scan alert repository is to keep all of the alerts in a JSON file (data/alerts.json). That JSON file holds a list of alerts for each package, and each alert holds a semver range representing the vulnerable range of that package along with the actual alert text. This file is actually fetched directly from GitHub by the Package Scan package when it’s installed in a project, which means that the alert repository can be updated without having to update Package Scan itself. To contribute an alert, just submit a pull request against alerts.json.

Read more about the package on the GitHub project page!

Black Box Meteor - Package Scanning

Written by Pete Corey on Apr 24, 2015.

An interesting side effect of Meteor’s packing system is that all packages used in a project are visible to the client. Open up a browser and take a look at the global Package object:

Object.keys(Package);

For a simple project, you may see results like this:

["underscore", "meteor", "json", "base64", "ejson", "logging", "reload", "tracker", "random", "retry", "check", "id-map", "ordered-dict", "geojson-utils", "minimongo", "ddp", "insecure", "mongo", "autoupdate", "meteor-platform", "autopublish", "webapp", "deps", "reactive-dict", "session", "livedata", "jquery", "htmljs", "observe-sequence", "reactive-var", "blaze", "ui", "templating", "spacebars", "launch-screen"]

So what are the consequences of this? Immediately, we see that this application is using autopublish and insecure. A malicious user could quickly couple a search for autopublish or insecure with a search for any globally defined Collections:

Object.keys(window).filter(function(key) {
    return window[key] instanceof Meteor.Collection;
});

Or just directly look through the Mongo collections published to the client:

Meteor.connection._mongo_livedata_collections

In a fraction of a second, a malicious user can see if your application is overpublishing, or vulnerable to arbitrary Mongo modifications through the autopublish and insecure packages.

But what about other packages? What if a malicious user is aware of a vulnerability in an existing Meteor package. Using the Package object on the client, that user can quickly check to see if an application is making use of that package, and is therefore vulnerable.

One sidenote is that the Package object does not include the version of the package being used. If a malicious users knows that a vulnerability exists in some version of a package but not in other versions, they would not immediately know if your application were vulnerable to their exploit.

So what can you do to protect your application? First, in nearly all cases your production application should not being using autopublish or insecure. Remove those packages. After that, always be sure to keep your packages updated to ensure that you’re not shipping code that may contain known flaws or vulnerabilities.