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" }
}
Now if at backend you are using
Model.findOne(req.body)
// or
Model.findOne({ username: req.body.username, password: req.body.password });
your application is vulnerable to NoSQL Injection. How? Let's substitute those values
Model
.findOne({
username: {
$ne: "malicious@example.com"
},
password: {
$ne: "mymaliciouspassword"
}
})
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]"
})
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)
...
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()
})
...
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
- Introduction to NoSQL Injection
- NoSQL Injection Payloads
- NoSQLMap - Automated NoSQL database enumeration and web application exploitation tool.
- NoSQLi Lab
- MongoSecure: An ExpressJS Middleware to Filter Out Malicious Payloads
Image has been taken from https://blog.sqreen.com
Top comments (9)
I'm not an experienced developer but wouldn't Schema Validation solve this? See: docs.mongodb.com/manual/core/schem...
Read this blog again, you will find that it works only on the
insertion
query. I have a solution forfind
andfindOne
queriesOh yeah. You're right.
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/...
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...
I've imported the module and followed the instructions but I get this error:
TypeError: mongoSecure is not a function
Please open an issue on the repository or share the code.
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.
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.