Putting the Brakes on NoSQL Injection with Secure Meteor

Pete Corey
Meteor Blog
Published in
6 min readApr 1, 2019

--

This guest post is by longtime Meteor community member Pete Corey, who is an independent consultant, web developer, and writer.

For the past three years I’ve been living and breathing Meteor security, and I’m excited to say I’ve managed to compile everything I’ve learned into a single comprehensive guide to securing your Meteor application. To celebrate the newly released Secure Meteor, I thought we could talk about one of the most prevalent and dangerous vulnerabilities that developers commonly introduce into their Meteor applications.

Let’s talk about NoSQL Injection!

To set the scene, let’s pretend that we’re building a Meteor-powered online storefront. Within our application, we’ve built a shopping cart system to let our users track and save the items they’re interested in purchasing. Like any other shopping cart system, we give our users the ability to empty their cart.

When a user clicks the “empty cart” button, their client makes a call to the emptyCart Meteor method, passing up the shopping cart’s MongoDB identifier. The emptyCart method simply removes the items (or “line items”) in that cart from the database, effectively leaving the cart empty:

Meteor.methods({
emptyCart(cartId) {
LineItems.remove({ cartId });
}
});

It’s important to realize that while our Meteor method assumes that cartId is a string, it’s never explicitly stating that assumption or making any assertion about cartId’s type.

A potentially malicious user could easily violate our assumption by manually calling the emptyCart method from their browser’s console and passing in any JSON-serializable value for cartId:

Meteor.call("emptyCart", { $gte: "" });

In this example, our malicious user has called the emptyCart method and specified a cartId of { $gte: ""}. Placing this value of cartId into our LineItems.remove query results in the following expression being executed on our server:

LineItems.remove({ cartId: { $gte: "" } });

Rather than deleting a single user’s items from their shopping cart, this removes every item from every shopping cart in the database, whether they belong to the user calling the method or not.

Manually calling our Meteor method from the browser.

The object the malicious user passed into the emptyCart method is a MongoDB query operator. Specifically, a “greater than or equal to an empty string” query operator. When dropped into our LineItems.remove query, it’s essentially telling the database to remove all line items in the database whose _id is “greater than or equal to an empty string.” Every line item’s _id will be greater than an empty string, so every line item will be removed.

Devastating.

This type of attack is known as a “NoSQL Injection” attack. NoSQL Injection vulnerabilities are incredibly dangerous and are particularly prevalent and relevant in Meteor applications, due to Meteor’s tight coupling to MongoDB and its seamless communication of disparate types through its DDP protocol.

Ultimately, NoSQL Injection attacks make their way into your application through missing, incomplete, or incorrect checking of user-provided data.

In this example, we could have easily prevented this NoSQL Injection attack by explicitly voicing the assumption we’re making about the cartId argument of the emptyCart method. Let’s add a check that asserts that cartId is, in fact, the string we’re assuming it to be:

Meteor.methods({
emptyCart(cartId) {
check(cartId, String);
LineItems.remove({ cartId });
}
});

By making an assertion about the type of cartId, we’ve completely prevented the possibility of a NoSQL Injection attack. If our malicious user tries to pass in their MongoDB query operator, they’ll receive a match error in response.

Match error when calling our Meteor method.

Let’s consider another example.

Imagine that we’ve decided to implement a user profile system in our shopping application. Letting our users tell us more about themselves might give us some valuable insights into their shopping habits!

We decided that the easiest route for implementing this feature was to add a set of “profile fields” to each user document, which they can freely edit to better express themselves. However, it’s important to remember that some fields on the user document are sensitive, and should only be updated by administrators.

Our first attempt at implementing this feature was with an editProfile Meteor method that accepts an update from the client, asserts that the client is able to perform the update, and then makes the modification to the current user’s document:

Meteor.methods({
editProfile(edit) {
if (edit.$set.isAdmin && !Meteor.user().isAdmin) {
throw new Meteor.Error("Not authorized.");
}
return Meteor.users.update(
{
_id: this.userId
},
edit
);
}
});

We’re making the assertion that only administrators are allowed to $set the isAdmin field on their user document. We wouldn’t want non-administrators escalating their privileges.

At first glance, this seems like a fine home-rolled solution.

Unfortunately, or fortunately, depending on your perspective, MongoDB gives us many ways of accomplishing the same task. While a malicious user might be forbidden from setting their isAdmin field to true directly, they can achieve the same result through other means.

Imagine our malicious user runs the following query from their browser’s console:

Meteor.call("editProfile", { $inc: { isAdmin: 1 } });

Our $set guard is bypassed because the malicious user is passing up an $inc update operator, so our editProfile method happily runs their query. Rather than setting their isAdmin field to a specific value, this query increments their isAdmin field, essentially turning it into a truthy value.

Our malicious user has succeeded in elevating their privileges to the administrator level with a simple NoSQL Injection attack.

There are many ways of fixing this vulnerability, and they all boil down to making assertions about the type and shape of our user-provided inputs. For example, we could check that edit is an object with a $set field, and $set is an object that optionally contains some known set of editable fields:

Meteor.methods({
editProfile(edit) {
check(edit, {
$set: {
phone: Match.Optional(String),
address: Match.Optional(String)
}
});
return Meteor.users.update(
{
_id: this.userId
},
edit
);
}
});

Now if anyone tries to use our editProfile to modify some unexpected fields, or pass up an unexpected MongoDB update operator, they’ll receive a match error and their method call will fail.

I hope that these examples have demonstrated that NoSQL Inject vulnerabilities can be both incredibly diverse in their effects, and incredibly devastating on your application.

At the end of the day, the root cause of NoSQL Injection attacks is always improperly validating user-provided input. It’s incredibly important to make strong assertions about the shape and type of any data provided by the client, especially if that data is going to be used to construct queries against your database. Failure to do so can result in data breaches, privilege escalations, and even full-scale Denial of Service attacks against your application!

If you’re eager to learn more about NoSQL Injection and other vulnerabilities that might be lurking inside your Meteor application, be sure to check out Secure Meteor.

I wrote Secure Meteor with the intention of sharing everything I’ve learned about Meteor security from my time spent working with amazing teams to better secure their Meteor applications. If this article, or Secure Meteor in its entirety helps you squash even one vulnerability in your application, I’ll chalk that up as a win.

--

--