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:
- The payload
{ "username": { "$ne": null }, "password": "dummy" }
uses the MongoDB$ne
(not equal) operator to check if theusername
is not equal tonull
. - 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:
- Use Parameterized Queries: Instead of building queries using raw user input, use parameterized queries or prepared statements that safely handle inputs.
- Use Schema Validation: Ensure that all inputs are validated against a predefined schema to restrict the types of values allowed.
- Use ORMs like Mongoose: ORMs provide built-in protection against injection attacks by validating data according to the schema definitions.
- 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()
andbody('password').isString().notEmpty()
validators ensure that theusername
andpassword
fields are non-empty strings. validationResult(req)
checks if there are any validation errors. If so, a400
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!