DEV Community

loading...
Cover image for One-step to prevent potential NoSQL Injection in your mongodb application

One-step to prevent potential NoSQL Injection in your mongodb application

tbhaxor profile image Gurkirat Singh Updated on ・3 min read

It starts with "Once Upon a Time" when I was learning MongoDB and thought that with the schemaless feature, it could be more secure than SQL Databases (SQL Injections). So I migrated all my projects to MongoDB.

Now, for the past few months, I have been working on NoSQL Injection and planning to start a series of tutorials on it.

What is Injection

An injection is a security vulnerability that lets attackers take control of database queries through the unsafe use of user input. It can be used by an attacker to: Expose unauthorized information. Modify data.

Let me show you a glimpse of NoSQL Injection at first.

Suppose, your application is accepting JSON username and password, so it can be bypassed by

{
    "username": { "$ne": "malicious@example.com" },
    "password": { "$ne": "mymaliciouspassword" }
}
Enter fullscreen mode Exit fullscreen mode

Now if at backend you are using

Model.findOne(req.body)

// or

Model.findOne({ username: req.body.username, password: req.body.password });
Enter fullscreen mode Exit fullscreen mode

your application is vulnerable to NoSQL Injection. How? Let's substitute those values

Model
  .findOne({ 
    username: { 
      $ne: "malicious@example.com" 
    },
    password: {
      $ne: "mymaliciouspassword"
    }  
})
Enter fullscreen mode Exit fullscreen mode

Now, if there is at least one document in the collection and not having the same username and password as the attacker has passed, it can log in to your web application with the very first document that matches this criterion

Practical Example: https://mongoplayground.net/p/omLJSlWfR-w

Preventing NoSQL

There is only one thing you can do, "SANITIZATION" by casting the input to in specific type. Like in this case, casting username and password to String() would work

As you know String() on any object would be [object Object] so I am directly substituting the value here

Model.findOne({
  username: "[object Object]",
  password: "[object Object]"
})
Enter fullscreen mode Exit fullscreen mode

In production, this would be the rarest document in the collection.

Practical Demonstration: https://mongoplayground.net/p/XZKEXaypJjQ

ExpressJS middle-ware approach

Four months ago I had created a question StackOverflow (https://stackoverflow.com/questions/59394484/expressjs-set-the-depth-of-json-parsing), to which a user named x00 posted the answer about the solution of setting up the depth of parsing nested JSON body.

Practical Demonstration

...
const depth_limit = 2; // the depth of JSON to parse

app.use(express.json())
const get_depth = (obj) => {
    let depth = 0
    for (const key in obj) {
        if (obj[key] instanceof Object) {
            depth = Math.max(get_depth(obj[key]), depth)
        }
    }
    return depth + 1
}

const limit_depth = function(req, res, next) {
    if (get_depth(req.body) > depth_limit) throw new Error("Possible NoSQL Injection")
    next()
}

app.use(limit_depth)
...
Enter fullscreen mode Exit fullscreen mode

Or if you want to use [object Object] notation to prevent application crash. I personally recommend you to use this one

...
let depth_limit = 1; // the depth of JSON to parse

app.use(express.json())
let limit_depth = (obj, current_depth, limit) => {
    // traversing each key and then checking the depth
    for (const key in obj) {
        if (obj[key] instanceof Object) {
            if (current_depth + 1 === limit) {
                obj[key] = "[object Object]" // or something similar
            } else limit_depth(obj[key], current_depth + 1, limit)
        }
    }
}

// middle-ware in action
app.use(function(req, res, next) {
    limit_depth(req.body, 0, depth_limit);
    next()
})
...
Enter fullscreen mode Exit fullscreen mode

Middle-ware in action

Practical Demonstration: https://repl.it/@tbhaxor/Preventing-NoSQL-Injection-in-Express

If you have some other cool ideas, I would love to hear from you. You can either comment down here or contact me at the following

References

Image has been taken from https://blog.sqreen.com

Discussion (9)

pic
Editor guide
Collapse
hjrobinson profile image
hjrobinson

I'm not an experienced developer but wouldn't Schema Validation solve this? See: docs.mongodb.com/manual/core/schem...

Collapse
tbhaxor profile image
Gurkirat Singh Author

Read this blog again, you will find that it works only on the insertion query. I have a solution for find and findOne queries

Collapse
hjrobinson profile image
hjrobinson

Oh yeah. You're right.

Thread Thread
hjrobinson profile image
hjrobinson

If I'm using a module like bcryptjs to make a comparison between passwords would this even matter because I'm not passing in a password to match against directly? It seems like the not equals thing wouldn't matter in this case. Of course not everyone is going to be using bcryptjs.

dev-to-uploads.s3.amazonaws.com/i/...

Thread Thread
tbhaxor profile image
Gurkirat Singh Author

While using this module, you would have to pass the replaceWith string. This will replace the nested object that exceeds the limit with that string.

Read the usage here: npmjs.com/package/@tbhaxor/mongo-s...

Thread Thread
hjrobinson profile image
hjrobinson

I've imported the module and followed the instructions but I get this error:

TypeError: mongoSecure is not a function

Thread Thread
tbhaxor profile image
Gurkirat Singh Author

Please open an issue on the repository or share the code.

Thread Thread
hjrobinson profile image
hjrobinson

I used a bare bones approach just using the code from your example in the repo and there doesn't seem to be an issue so I think it's some sort of conflict with my existing code, another module, or something not being up to date. It's not a top priority for me to narrow down the problem right now but if I ever get back to it I'll post an issue in the repo.

Thread Thread
hjrobinson profile image
hjrobinson

For the record your middleware code seems to do the trick without having to use the mongo-secure module. Without the middleware the password in my example would still be protected from injection because bcrypt.compareSync returns:

{
"message": "Illegal arguments: object, string"
}

However, if the attacker knew the password and not the email there would still be a threat so your middleware code still comes in handy for my use case. I don't know if the middleware will affect my other endpoints but I'm sure it will be revealed if it does. Thanks for the post.