NoSQL injection is a type of vulnerability where an attacker is able to inject arbitrary text into NoSQL queries.
NoSQL injections are very similar to the traditional SQL injection attack, except that the attack is against a NoSQL database. NoSQL is a general term for any database that does not use SQL, a common database management system (DBMS) that utilizes NoSQL is MongoDB.
In this post, Iād like to describe the followings to show how to prevent NoSQL injection:
- Build a simple NodeJS app
- Understand what a NoSQL Injection attack is
- Use a
npm
package to prevent attacks
For prerequisites, let us assume that we have a ready-to-use MongoDB cluster or a local MongoDB installation and have the connection URI, e.g. http://localhost:27017.
Project Setup
Let us initialize our node.js app and install a couple of packages. Run the following commands in an empty folder.
$ npm init -y
$ npm i express mongoose
$ npm i nodemon dotenv -D
Or, use Yarn
instead.
$ npm init -y
$ yarn add express mongoose
$ yarn add nodemon dotenv ā-dev
Create app.js
and models/user.model.js
.
app.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const routes = require('./routes/index');
const app = express();
app.use(express.json());
app.use(routes);
mongoose
.connect(process.env.MONGODB_URI)
.then(() => {
console.log('Mongoose connected š');
app.listen(3000, () => {
console.log('Server is up and running š');
});
})
.catch((error) => {
console.log(error);
});
models/user.model.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: {
type: String,
unique: true
},
password: {
type: String,
unique: true
}
});
const User = mongoose.model('User', userSchema);
module.exports = User;
With the setup out of the way, let us start by looking at a simple NoSQL attack.
For demonstration purposes, I'll create a simple routing file.
routes/index.js
const express = require('express');
const User = require('../models/user.model');
const router = express.Router();
router.get('/users', async (req, res, next) => {
return res.json({
users: await User.find({}).exec()
});
});
router.post('/login', async (req, res, next) => {
const { username, password } = req.body;
const user = await User.findOne({ username, password }).exec();
res.json({
message: `Logged in as ${user.username}`
});
});
module.exports = router;
It's time to make a GET request to /users
to see what we have. As mentioned previously, we will be using Postman.
// GET localhost:3000/users
{
"users": [
{
"_id": "6125401c491c0b0bfd123165",
"username": "test1",
"password": "123456",
"__v": 0
},
{
"_id": "6125401c491c0b0bfd123166",
"username": "test2",
"password": "abcdef",
"__v": 0
},
{
"_id": "6125401c491c0b0bfd123167",
"username": "test3",
"password": "sdjndsn",
"__v": 0
}
]
}
This is the response that the server returns. As seen from the code block above, we have three users (Passwords are shown for demonstration purposes) for example.
Next, try to log in using one of the credentials. Make a POST request to /login
with the request body below.
{
"username": "test1",
"password": "123456"
}
We would get the following response. Everything seems pretty normal up to this point, so let us dive deeper!
// POST localhost:3000/login
{
"message": "Logged in as test1"
}
Perform a demo NoSQL injection!!!
To perform a NoSQL injection attack, simply change the password value in the body from "123456"
to { "$ne": "null" }
. So, the new request body looks like the following:
{
"username": "test1",
"password": { "$ne": null }
}
Hold on, please!
What does the $ne
operator do? Let me explain.
$ne
operator tells mongo to check for not equal to. That is, instead of checking the password for the correct value, MongoDB will simply check whether the password field is null or not. Since the password field cannot be null in any case, we can trick MongoDB into giving us the user information without knowing the password.
After making a POST request to /login
with the new modified request body, we would get the following response from the server.
// POST localhost:3000/login
{
"message": "Logged in as test1"
}
Oh my god!š±
There is more than one way to do the same thing. For instance, we could have used { "$gt": "" }
instead of the $ne
operator.
Fortunately, preventing this attack is as easy as performing it. Next, let us take a look at how we can prevent NoSQL injection attacks.
Introduce NPM module
We would install the express-mongo-sanitize
package from NPM. Run the following command:
$ npm i express-mongo-sanitize
Or,
$ yarn add express-mongo-sanitize
We can learn the details about this module here:
https://www.npmjs.com/package/express-mongo-sanitize
Requiring the package returns a middleware function, which can then be used as in the example below.
// Prevent NoSQL injection
const mongoSanitize = require('express-mongo-sanitize');
...
// Prevent NoSQL injection
app.use(
mongoSanitize({
onSanitize: ({ req, key }) => {
// Throw an error
// Catch with express error handler
}
})
);
I hope this post would be a bit helpful to prevent NoSQL injection attacks when you implement your node.js express server.
For a full source code, please refer to this GitHub repo:
https://github.com/liyang51827/express-nosql-injection-demo
Top comments (0)