NoSQL injections: how MongoDB queries can be exploited

When people talk about database security, SQL injection is usually the first thing that comes up. Most developers have learned, one way or another, how easily a database can be exploited through user input. What gets talked about a lot less is that NoSQL databases can be injected too. In this article, I'll walk through how NoSQL injection works, using MongoDB as the example.

A quick refresher on SQL injection

SQL injection is when an attacker manipulates a SQL query by injecting malicious input. For example, take a simple query for user authentication:

SELECT * FROM users WHERE username = '$username' AND password = '$password';

If an attacker enters admin' OR '1'='1 as both the username and the password, the query becomes:

SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'admin' OR '1'='1';

This bypasses authentication entirely and lets the attacker in. These days we don’t worry about SQL injection as much: developers are well aware of it, and most frameworks and libraries protect against it out of the box. The same can't be said for NoSQL injection, which can be just as damaging but stays under the radar for a lot of people.

What is NoSQL injection?

NoSQL injection is the same idea as SQL injection, but it targets NoSQL databases. These databases usually accept JSON-like structures, which opens the door to a different set of vulnerabilities.

Here's a typical MongoDB query:

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

In a perfect world, the server would receive a request like this:

{"username": "john.doe", "password": "I_like_bananas_123"}

At first glance, this looks harmless. We're dealing with an object, not a string, so there's no room to inject something that changes what the query does. Well, not quite. This code is actually vulnerable to NoSQL injection.

The injection

One way to exploit this query is to send a request like this:

{"username": {"$ne": null}, "password": {"$ne": null}}

In MongoDB, operators like $ne are used for different kinds of comparisons. $ne means "not equal": it matches when the field is not equal to the given value.

So the query becomes:

users.findOne({ username: { $ne: null }, password: { $ne: null } });

This returns any user, since it's only asking for a username that isn't null and a password that isn't null. The result: the attacker gets into the system, and potentially gets access to sensitive user data.

The fix: validate user input

So how do we protect against this? Validate your input. Always make sure user input is validated and sanitized before it reaches your queries. (Obviously, duh!)

A good way to do this is with a library like Zod, which makes validation simple and gives you type safety out of the box:

const loginSchema = z.object({
  username: z.string().min(1),
  password: z.string().min(1),
});

// Validate the incoming request body
const result = loginSchema.safeParse(req.body);

if (!result.success) {
  // Handle validation errors
  return res.status(400).send(result.error.errors);
}

// Proceed with the safe query
users.findOne({ username: result.data.username, password: result.data.password });

Validating the input means only well-formed data gets through, which greatly reduces the risk of NoSQL injection.

Conclusion

NoSQL injection is a real threat, and it's easy to overlook because object-based queries feel safer than SQL strings. Using MongoDB doesn't mean you can skip input validation. Validate what comes in, and you'll keep your app safe from these attacks.