Preventing NoSQL Injection Attacks in Node.js: A Real-World Demonstration

Bilal Khan
3 min readSep 27, 2024

--

NoSQL injection is a type of security vulnerability that can occur in applications that use NoSQL databases like MongoDB. Similar to SQL injection in relational databases, NoSQL injection allows an attacker to manipulate a query and gain unauthorized access to data or bypass authentication mechanisms.

In this article, I’ll demonstrate how NoSQL injection works and why it’s important to implement proper security measures when developing with NoSQL databases like MongoDB.

Example Application with Vulnerability

Let’s consider the following code snippet of an Express application using MongoDB and Mongoose. This application exposes a registration and login API for users.

const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const User = require('./user.model');

const app = express();
app.use(bodyParser.json());
const port = 8080;

app.get('/', (req, res) => {
res.send('Hello World!');
});

// User registration endpoint
app.post('/api/user', (req, res) => {
const user = new User({
username: req.body.username,
email: req.body.email,
password: req.body.password
});
user.save()
.then(() => res.send(user))
.catch(err => res.status(400).send(err));
});

// User login endpoint
app.post("/api/user/login", (req, res) => {
const user = User.findOne({ username: req.body.username, password: req.body.password })
.then(user => {
if (!user) {
return res.status(404).send();
}
res.send(user);
})
.catch(err => res.status(500).send(err));
});

mongoose.connect('mongodb://127.0.0.1:29734/test').then(() => {
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
});

What’s Wrong with This Code?

If you take a closer look at the login endpoint, you’ll notice that the application directly uses the username and password fields from the request body to construct a MongoDB query:

User.findOne({ username: req.body.username, password: req.body.password })

An attacker can manipulate this query to inject additional MongoDB operators or bypass the authentication logic entirely.

Demonstrating NoSQL Injection

Let’s see how an attacker could exploit this vulnerability. Suppose we have the following user in our database:

{
"username": "admin",
"password": "securepassword",
"email": "admin@example.com"
}

Exploit Using cURL

With this vulnerable login API, the attacker can send a specially crafted request that bypasses the authentication process:

curl -X POST http://localhost:8080/api/user/login \
-H "Content-Type: application/json" \
-d '{"username": {"$ne": null}, "password": "dummy"}'

Explanation:

  • The payload { "username": { "$ne": null }, "password": "dummy" } uses the MongoDB $ne (not equal) operator to check if the username is not equal to null.
  • Since this condition will be true for all documents, the query will return the first user it finds in the database, effectively allowing the attacker to log in as the first user (in this case, admin), regardless of the password provided.

Server Response

The above command will return the following response:

{
"_id": "5f5a...29e2",
"username": "admin",
"email": "admin@example.com",
"password": "securepassword"
}

The attacker has successfully logged in as the admin user without needing the actual password!

Preventing NoSQL Injection

To prevent NoSQL injection attacks, you should sanitize user input and avoid passing untrusted data directly to MongoDB queries. Here are some best practices:

  1. Use Parameterized Queries: Instead of building queries using raw user input, use parameterized queries or prepared statements that safely handle inputs.
  2. Use Schema Validation: Ensure that all inputs are validated against a predefined schema to restrict the types of values allowed.
  3. Use ORMs like Mongoose: ORMs provide built-in protection against injection attacks by validating data according to the schema definitions.
  4. Limit Operators: Restrict the use of certain MongoDB operators in user input, such as $ne, $eq, $gt, and others.

Securing the Example Code

In our example, we can use Mongoose’s validation and libraries like express-validator to sanitize and validate the input. Here's a secure version of the login endpoint:

const { body, validationResult } = require('express-validator');

app.post("/api/user/login",
body('username').isString().notEmpty(),
body('password').isString().notEmpty(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { username, password } = req.body;
User.findOne({ username, password })
.then(user => {
if (!user) {
return res.status(404).send();
}
res.send(user);
})
.catch(err => res.status(500).send(err));
}
);

Explanation of the Secure Version

  • The body('username').isString().notEmpty() and body('password').isString().notEmpty() validators ensure that the username and password fields are non-empty strings.
  • validationResult(req) checks if there are any validation errors. If so, a 400 the response is returned, preventing malicious input from reaching the database query.

Final Thoughts

NoSQL injection is a serious vulnerability that can compromise the security of your application. Always sanitize and validate user input and follow best practices to protect your application against such attacks.

This demonstration highlighted how easy it is to exploit unprotected NoSQL queries and how to secure your application with proper validation and sanitization. If you’re building applications with NoSQL databases like MongoDB, take the time to ensure your queries and prevent potential threats.

Let me know if you have any questions or if there’s any other topic you’d like to see covered!

--

--