This material is now available in the Pluralsight.com course "Securing Your Node.js Web Application".Pluralsight Course Securing Your Node.js Web Application

Securing Node.js: MongoDB Security from Injection Attacks

MongoDB Security from Injection Attacks Feature Image

Due to the high popularity of RDBMS based databases and the lack of adoption of NoSQL database, the notion of SQL injection attacks became the defecto known type of injections attacks.  Because of this, a myth formed that NoSQL database such as the popular MongoDB were invulnerable to injection attacks when they started seeing widespread use.  MongoDB security is a vital area in the overall security health of your application.

In this post, we’re going to specifically look at protecting our MongoDB from injection attacks. Before we do, lets take a quick look at why NoSQL databases are no less vulnerable to Injection attacks than RDMBS database and some would argue, more susceptible.

Why NoSQL Databases are Vulnerable to Injection Attacks

The main mitigation to SQL injections in RDBMS’s that use structured query language is the ability to define parameterized queries at the database engine level.  Unfortunately, this is not a capability of NoSQL databases which are heavily dependent on the custom drivers develop to facilitate NoSQL API calls.  It’s here that the injected information is parsed and evaluated and makes the database vulnerable of unexpected instructions being executed.

Furthermore, to interface with a NoSQL database’s API, the API calls are based on the applications language or a specific convention such as JSON, XML and BSON.  NoSQL-databases.org list over 225 different NoSQL types of databases, each with their own language and relationship models. Because there isn’t a common language between them, protecting and testing requires knowledge specific to the database, syntax, data model and presumably application programming language.

MongoDB Security

This doesn’t have to be rocket science as there is two main points to embrace to avoid injection attacks in your MongoDB database:

  1. Avoiding specific MongoDB operators and expressions
  2. Properly handling untrusted data

MongoDB stores information as JSON documents.  Which provides support for all primary data types.  But MongoDB doesn’t stop there, it stores them as BSON (binary-encoded JSON documents).  As BSON documents, MongoDB protects itself from traditional injection attacks because of how it encodes special characters. Despite the use of BSON as a storage format, MongoDB is not out of the clear.

Avoiding Specific MongoDB Operators and Expressions

Unfortunately, there are some operators and their behavior by MongoDB, that allow injection attacks to still be a serious risk.

Lets look at the following SQL non-parameterized query:

SELECT * FROM Products WHERE Name = 'Search-Term'

 

A SQL injection attack could be provided through the “Search-Term such as:

SELECT * FROM Products WHERE Name = 

    ‘flash light’; update products set name =  ‘All your bases are belong to us'

 

The end result is that an expression in the form of a command is executed that causes product names to be updated.  Now, I showed this so that we can compare it to the following MongoDB query:

db.products.find({$where: `this.name == ${user-input}`})

 

Look similar?  Essentially, just as the SQL injection attack example, the user-input in the above MongoDB find query could evaluate to any arbitrary JavaScript expression.

Very closely in the underlying nature of how JavaScript’s native Eval() function works, MongoDB provides a few operators that allow arbitrary JavaScript Expressions to be ran.  Specifically, the one we need to be aware of is:

  1. $where operator

But lets look at how this works by seeing an actual real-world example

 

MongoDB JavaScript Express Example

Lets take the following Express.js route that handles GET requests for Timeline items.

timelineRouter.route("/api/timeline")
    .get(async function (req, res) {
        try {
            const {startDate, endDate} = req.query;
            const query = {$where: "this.hidden == false"};

            if (startDate && endDate) {
                query["$where"] = "this.start >= new Date('" + startDate + "') && " +
                        "this.end <= new Date('" + endDate + "') &&" +
                        "this.hidden == false;";
            }

            const TimelineItem = await getTimelineItemModel();
            const timelineItems = await TimelineItem.find(query);
            console.log(colors.yellow(`# of Timeline Items retrieved: ${timelineItems.length}`));
            return res.json({timelineItems: timelineItems});

        } catch (error) {
            res.status(500).send("There was an error retrieving timeline items.  Please try again later");
        }
    });

Basically, we want to return all Timeline documents that are not hidden and the document start and end date falls within the provided start and end date (if dates are provided). We do this by generating a query object that contains the properties we want to search for.

 

$where

query["$where"] = "this.start >= new Date('" + startDate + "') && " +
                        "this.end <= new Date('" + endDate + "') &&" +
                        "this.hidden == false;";
            }

Here we are setting the query object with a $where property and when evaluated by MongoDB, will equate to the $where operator.  We’re setting the value to a JavaScript expression of:

"this.start >= new Date('" + startDate + "') && " +"this.end <= new Date('" + endDate + "') 
&&" +"this.hidden == false;";

 

The Problem

Since the startDate and the endDate are being provide, imagine if we provided the following value for either one of these request parameters:

"');return true;}+//

Essentially, this will change the query object $where property value to the following final value:

"this.start >= new Date('" + startDate + "') && " +"this.end <= new Date('" + endDate + "');return true;}+//

The result is that we have just returned any and all Timeline documents, no matter if they are hidden or not.  Therefore, in this example, no matter how many documents were hidden, we circumvented the constraints to not server any hidden documents.

The Solutions

We have three courses of action we can take, two of which we’ll look at here and the third in the last part of this article.

Solution #1: Alternate Query Construction

Don’t use the $where operator.  As MongoDB clearly points out there is usually always a better way to create the same query without the use of the $where operator.

Fix

Here is the same route and query designed without the $where operator and use of JavaScript expressions:

timelineRouter.route("/api/timeline(/:id/)?")
    .get(async function (req, res) {
        try {
            const {startDate, endDate} = req.query;
            const query = {hidden: false};
            if (startDate && endDate) {
                query["start"] = {$gte: startDate};
                query["end"] = {$lte: endDate};
            }

            const TimelineItem = await getTimelineItemModel();
            const timelineItems = await TimelineItem.find(query).exec();

            res.json({timelineItems: timelineItems});
        } catch (error) {
            console.log(colors.red("There was an error retrieving timeline items: " + error));
            res.status(500).send("There was an error retrieving timeline items.  Please try again later");
        }
    });

 

What did we do?
  • Removed $where operator
  • No longer used an arbitrary JavaScript expression
  • Set the start and end dates as separate properties to search for on the document
  • and used the MongoDB $gte (greater than or equal) and $lte (less than or equal) comparison operators to set the query parameters.
NOTE: Prior to the older MongoDB 2.4, the $where operator had access to global operators and properties such as the highly volatile “db” that would allow for even more devastating results.

 

Solution #2: Database Wide Constraints

In addition to one-off fixes as the one in Solution 1, we can help better protect our application from ourselves, specifically developers who might not know better or unrealized circumstances that were missed.

We can implement a database wide constraint that won’t allow arbitrary JavaScript expression to be ran.   By using a mongod.conf file we can set the javascriptEnabled property to false such as below:

mongod-config-file.conf
security:

  javascriptEnabled:false

When starting our MongoDB server mongod we can set the config file

mongod   --config/path/to/config-file.conf

This way, whether a misinformed developer introduces the vulnerability, it will quickly be squashed when it fails to execute.

But, avoiding vulnerably MongoDB operators and arbitrary JavaScript expressions is not the only way we can and should handle these circumstances.  Which brings us to the other additional way we need to tackle this issue.

Properly Handling Untrusted Data

Unfortunately, this article is about tackling direct injection threats to MongoDB.  While I would argue that the topic of handling untrusted data is just as important, an up coming article will be tackling this expansive topic. But, it would not be right of me to not mention that handling untrusted data is a very crucial part of mitigating threats such as injection attacks.

However, as you will find over time working at securing your application, a vast majority of web based security risks that your application faces, hinge on untrusted data being introduced to your application.  Properly handling that data is key to mitigating such threats and our Express.js route example is no different.

But, instead of leaving you completely high and dry, let me leave you with the following information.

Identifying Untrusted Data

The first step in handling untrusted data is identifying it.  As we’ll see in the article on handling untrusted data, it is a lot harder than one probably would expect.

In our express.js route example, its very clear what is untrusted data – the provided parameters startDate and endDate.

try {
    const {startDate, endDate} = req.query;
    //…removed for brevity

This can be an values provided by the user and falls outside of our trust boundary.  Another term will look at in the near future.  This is an area in which we have an elevated trust and a mechanism we can use to help identify untrusted data.

Solution: Validating Against Whitelist

In this case, we have a preconceived notion of what would be acceptable values for these parameters.

Validating the startDate or endDate match only values we expect, such as a date, we are validating against a whitelist.  This is much more secure and manageable as opposed to a blacklist of values we want to avoid.

Depending on your server side framework of choice, would dictate available libraries to do the heavy lifting of validating untrusted data.  But, as we’ll see for an express.js application we can leverage NPM packages such as express-validator to validate parameters against a set of rules called a schema.

export const timelineRangeSchema = {
    "startDate" : {
        isDate: true,
        notEmpty: false
    },
    "endDate" : {
        isDate: true,
        notEmpty: false
    }
};

 

But all of this and much, much more in an upcoming article on handling untrusted data.

Conclusion

As we seen NoSQL database such as the popular MongoDB, is just as vulnerable to injection attacks as we see with many RDBMS databases.  As with SQL injection attacks which are based on the ability to execute query expressions that have unexpected consequences, MongoDB has the ability to execute arbitrary JavaScript expressions through specific operators.

The key to avoiding injection threats in our MongoDB database, as well as part of the overall MongoDB security, is avoiding the use of arbitrary Javascript expressions from being executed. This can be accomplished by both avoiding MongoDB operators that allow JavaScript expressions as well as implementing a database wide restriction through the use of mongod security settings. We also seen that another separate, by highly relatable mitigation is handling any untrusted data that might be part of the query being evaluated by the MongoDB engine.

About the author

Max McCarty

Max McCarty is a software developer with a passion for breathing life into big ideas. He is the founder and owner of LockMeDown.com and host of the popular Lock Me Down podcast.