Black Box Meteor - Shared Validators

Written by Pete Corey on Jun 29, 2015.

Discover Meteor’s recent blog series about allow and deny security has done a great job at raising awareness around the security concerns that surround collection validators. Check out their Allow & Deny: A Security Primer post for a rundown on validator security.

Isomorphic Woes

One question I’ve asked myself is where do we put these allow and deny methods within our Meteor application? Intuitively, we may think that the best place to keep them is where we’ve defined our collections - in a shared location visible to the client and server. This seems to be a very common pattern amongst Meteor developers. You can even see it in Sacha’s allow & deny challenge final example. He’s defining his allow and deny methods for the Messages collection in common.js.

So what’s the big deal? Why does this matter? Well, imagine if your allow and deny methods weren’t completely secure. Would it be a good idea to ship those methods down to the client where any malicious user could browse through them at their convenience? Probably not, but that’s exactly what’s happening.

Open the allow and deny challenge MeteorPad, and in your browser’s console run the following statement:

> Messages._validators.update.allow[1].toString();
< "function (userId, doc, fields, modifier) {
    
    // log out checks
    console.log("// All checks must return true:");
    console.log(!!userId);
    console.log(!_.contains(doc.likes, userId));
    console.log(_.keys(modifier).isEqualTo(["$addToSet", "$inc"]));
    console.log(_.keys(modifier.$addToSet).isEqualTo(["likes"]));
    console.log(_.keys(modifier.$inc).isEqualTo(["likesCount"]));
    console.log(modifier.$addToSet.likes === userId);
    console.log(modifier.$inc.likesCount === 1);
    
    var check = 
      userId &&
      !_.contains(doc.likes, userId) &&
      _.keys(modifier).isEqualTo(["$addToSet", "$inc"]) &&
      _.keys(modifier.$addToSet).isEqualTo(["likes"]) &&
      _.keys(modifier.$inc).isEqualTo(["likesCount"]) &&
      modifier.$addToSet.likes === userId &&
      modifier.$inc.likesCount === 1;
      
    return check;
  }"

You’ll notice that the entire source of the allow function is visible to the client! Take some time and explore the _validators object. You’ll notice that all allow and deny methods for the Messages collection are being passed to the client.

Server-side Solution

The correct place to keep your allow and deny methods is on the server. Peruse through the official docs and read the wording surrounding the allow and deny methods. Notice that they’re specifically marked as server functionality.

When a client calls insert, update, or remove on a collection, the collection’s allow and deny callbacks are called on the server to determine if the write should be allowed.

By keeping your methods on the server, a malicious user will not be given to opportunity to dig through them with a fine-toothed comb. They would be reduced to manual testing or fuzzing to find vulnerabilities in your collection validators.

I feel it’s important for me to mention that I’m not advocating hiding your allow and deny methods as an alternative to properly securing them. You should do your absolute best to correctly secure your validators. Moving them to the server simply gives you a small layer of protection against attackers and makes their job slightly harder. Remember, security through obscurity is not security.

Meteor Club Podcast - Talking Security

Allow & Deny Challenge - Check Yourself

Written by Pete Corey on Jun 15, 2015.

If you read Crater, or follow the Discover Meteor blog, you probably saw Sacha Greif’s recent Allow & Deny Security Challenge. If you haven’t taken the challenge yet, go do it now! It’s a great way to flex your Meteor security muscles. Plus, it really opens your eyes about how careful you need to be when writing allow & deny rules for your Meteor collections.

I decided to have some fun with the challenge and based my implementation on Meteor’s check package. Before we dive into my solution, here’s my MeteorPad submission, and here’s the allow function in its entirety:

Messages.allow({
    update: function (userId, doc, fields, modifier) {
        var checkEdit = {
            $set: {
                body: String
            }
        };

        var checkLike = {
            $addToSet: {
                likes: Match.Where(function(likes) {
                    check(likes, String);
                    return likes == userId &&
                           !_.contains(doc.likes, userId);
                })
            },
            $inc: {
                likesCount: Match.Where(function(likesCount) {
                    check(likesCount, Number);
                    return likesCount == 1;
                })
            },
        };

        if (userId == doc.userId) { // Like or edit
            return Match.test(modifier, Match.OneOf(checkEdit, checkLike));
        }
        else { // Like
            return Match.test(modifier, checkLike);
        }
    }
});

The high level plan of attack for this allow method is to permit a user to either edit the body of their own post, or like a post. A user may only like a post once, and a user may not like and edit their post in a single update. To implement these restrictions using check, we need to think about what the modifiers for these two actions will look like.

The Edit Message Pattern

The modifier for updating your message is very simple. We’re expecting a $set on the body field. Additionally, we’re expecting body to be a String. Written as a pattern, it would look like this:

var checkEdit = {
    $set: {
        body: String
    }
};

The Like Message Pattern

The modifier for liking a message is slightly more complicated. We expect the current user’s userId to be added to the likes field using the $addToSet operator, and we expect the likesCount field to be incremented by one. We can use Match.Where to assert that the userId we’re adding to the likes field is a String, is equal to the current user’s userId, and doesn’t already exist in the array:

$addToSet: {
    likes: Match.Where(function(likes) {
        check(likes, String);
        return likes == userId &&
               !_.contains(doc.likes, userId);
    })
}

We can also use Match.Where to make sure we’re only adding 1 to likesCount:

$inc: {
    likesCount: Match.Where(function(likesCount) {
        check(likesCount, Number);
        return likesCount == 1;
    })
}

All together, the modifier we expect when liking a message should match this pattern:

var checkLike = {
    $addToSet: {
        likes: Match.Where(function(likes) {
            check(likes, String);
            return likes == userId &&
                   !_.contains(doc.likes, userId);
        })
    },
    $inc: {
        likesCount: Match.Where(function(likesCount) {
            check(likesCount, Number);
            return likesCount == 1;
        })
    },
};

Apply The Patterns

The last section of the allow method applies these patterns to the modifier we’ve been given. In my original submission, I implemented this section as two calls to check wrapped in a try/catch block. If either of the checks failed, the catch block would prevent the thrown exception from bubbling up and return a false to prevent the update. Later, I realized that I could use Match.test instead.

If the user making the update owns the document, we use Match.OneOf to allow them to either edit the message or like the message:

if (userId == doc.userId) {
    return Match.test(modifier, Match.OneOf(checkEdit, checkLike));
}

Otherwise, we only let them like the message:

else {
    return Match.test(modifier, checkLike);
}

And that’s it!

Final Thoughts

Originally, this was intended as a just-for-fun experiment, but the check approach is beginning to grow on me. The idea of using patterns to describe the modifier for each action and then applying the appropriate pattern based on the user’s permissions seems very readable and easy to understand at a glance. I might make use of this pattern in the future.

Check is a suprisingly powerful library capable of preventing a wide variety of Meteor security issues if used correctly. I highly recommend reading through the docs and brushing up on your checking skills!