A NoSQL Injection Primer (with Mongo)
Last year, I interviewed a number of coding bootcamp graduates who were taught the MEAN stack exclusively. When looking at their final projects, all of them had NoSQL injection vulnerabilities present, indicating that perhaps the schools were not teaching secure coding.
When I asked them about this (gently, because it was definitely not their fault, and I made clear was not part of my assessment but for my personal information), every candidate had at least heard of SQL Injection, but were not aware that Mongo (and other data stores) had a separate class of injection attacks known collectively as NoSQL Injection.
This might be because NoSQL Injection hasn't had as much press as classical SQL Injection, though it should. Although traditional SQL databases still dominate the overall usage statistics, DB-engines.com has Mongo listed as the 5th most popular datastore, with several other NoSQL engines in the top ten.
Because Mongo currently has the largest footprint, I focus here on Mongo injections. There doesn't exist a NoSQL language standard, so injections for each vendor differ depending on the query language used and things like client permissions and data structure.
NoSQL Injections with PHP
Most NoSQL injection examples across the web leverage PHP, and I'll start there as well. PHP has a language feature that allows the end user to change GET querystring inputs into arrays by changing the URL parameters into parameters with array brackets:
http://test.com/page?parameter=value // normal URL
http://test.com/page?[parameter]=value // PHP treats input as an array now
This is important because Mongo uses array syntax to declare verbs. For instance, an insecure PHP app might query like this:
$param = $_GET['parameter'];
$query = [ "data" => $param ];
// Example of PHP REGEX search syntax, which we want to inject
// $query = [ 'data' => [ '$regex': => '\d+' ]];
$query = new MongoDB\Driver\Query($query, []);
In PHP we can replace parameter with [$regex]
and the value with a regular expression to search, and PHP will create a query that looks like ["data" => ['$regex': => 'searchValue']]
which allows an injection. From here, we can do things like make the query always true, or ask questions about the data to extract it.
Of course, regex isn't the only verb that can be injected - any supported querystring can be leveraged in this way, though regex is often a very useful one to inject for data extraction.
A Vulnerable NodeJS App with Mongo
Other languages don't have this same feature, so attempting this same injection pattern won't work, even if the app is coded insecurely. We still look for this kind of injection, though it typically is passed via JSON instead of GET parameters.
To illustrate other methods of injection, I wrote a simple NodeJS application that is vulnerable to two kinds of NoSQL injection. If you want to run this app locally to follow along, you will need docker and docker-compose. Clone and build the images and navigate to http://localhost:4000:
git clone https://github.com/Charlie-belmer/vulnerable-node-app/tree/master/app
cd vulnerable-node-app
docker-compose up
One thing many users are surprised about, is that Mongo supports JavaScript evaluation when a JS expression is placed into a $where
clause or passed into a mapReduce
or group
function. So anywhere unfiltered input is passed to one of these clauses, we may find JavaScript injection.
JavaScript Injection Example
The app has a JavaScript injection in the querystring of the user lookup page. The (vulnerable) lookup code looks like this:
let username = req.query.username;
query = { $where: `this.username == '${username}'` }
User.find(query, function (err, users) {
if (err) {
// Handle errors
} else {
res.render('userlookup', { title: 'User Lookup', users: users });
}
});
As you can see, the username search string is pulled directly from the request without any filtering. The query is a where clause which passes in the username string directly. So, if we put valid JavaScript into the querystring, and match quotes correctly, we can have Mongo execute our JavaScript!
In this case, our goal is to find all valid users, so we'd like to pass in something which will always evaluate to true. If we pass in a string like ' || 'a'=='a
the query will become $where: `this.username == '' || 'a'=='a'`
which of course always evaluates to true and thus returns all results. With JS injection, there are many other things we might be able to achieve, and boolean checks are just one.
Verb Injection with Node & Mongo
I mentioned previously that changing the querystring to include brackets doesn't work with Node and Express. This app is using Express, Mongo, and Node, three of the four components of MEAN stack applications, with the last being Angular. Many apps built with Angular communicate with the Node service using JSON objects instead of GET requests.
Mongo queries are mostly just JSON themselves, so we can modify JSON objects in transit to attempt injections. The login page passes a username and password as strings within a JSON object. An expected object looks like this:
{"username":"myaccount","password":"password"}
And the receiving code in express looks like this:
let query = {
username: req.body.username,
password: req.body.password
}
User.find(query, function (err, user) {
if (err) {
// handle error
} else {
if (user.length >= 1) {
res.json({role: user[0].role, username: user[0].username, msg: "Correct!" });
}
}
});
Again, the body JSON content is parsed and passed directly to Mongo. We can replace the username or password field with a valid Mongo query JSON object to inject something. This time, instead of using the regex verb, we'll use the $ne (not-equals) verb to make the password always correct. We modify this by catching the request with a proxy like ZAP or Burp, and modifying the body to the following JSON:
{"username":"myaccount","password":{"$ne": 1}}
Now we can login to the myaccount user without knowing the password, and could use regex requests to extract the password with a few iterations.
Preventing NoSQL Injections In Your Code
As with most injection attacks, NoSQL injections can be prevented by using proper filtering techniques. There are a few things I recommend to harden your mongo instance and application code. This will limit or prevent injections in your code, regardless of language or framework.
- Don't use the Mongo
where
,mapReduce
, orgroup
with user supplied data. These are all JavaScript injectable functions. Where clauses can almost always be re-written as normal queries, perhaps usingexpr
instead. See the Mongo documentation. - Use a typed model. Typed models will automatically stop some injections by converting user input to the expected type, such as string or int. Note that in my sample project, I did use a typed model and it did not prevent the attacks shown, so it is of limited use (but better than not using a typed model).
- Set
javascriptEnabled
tofalse
in your mongod.conf, if you can. This will disable JavaScript execution in your instance and remove that class of attacks. - Always strongly validate user supplied data - this will help prevent a lot more than just NoSQL attacks! Use libraries like mongo-sanitize (node) or similar libraries on all user supplied information, including cookie values and data from the browser. If you can't find a library, then enforce type and escape problematic characters within input like single and double quotes. This is difficult to get right, so only do this if your language and framework don't offer good sanitization libraries.
Conclusions
This is an introductory post about NoSQL Injections, and serious attackers are likely to use far more advanced attacks than shown here.
Still, I hope that MEAN stack developers and NoSQL users will pay attention to this class of attacks and take steps to limit the impact on their applications.
The tools in this space are also somewhat limited in my opinion, which is why I started work on an open source injection tool, which I will be discussing in a future post when it is a little further along.
Have other things to share about NoSQL injections? I would love to hear in the comments!