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

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:


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


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!