The Power of Secure Coding Practices: Safeguarding MongoDB Against Exploitation
https://github.com/kiliczsh/nosql-injection
Hello everyone, I need to tell a little story first:
Several months ago I was working on a side project for automating the election results display. I had a quick development process and published the website with Ampt and the database server. My Security Researcher friend Sven immediately sent the URL he exploited the application and I was kind of surprised as I was using default queries nothing very specific but the result was there, he was blocking my application to proceed with data with fun! Thanks, Sven :)
I understood that there are many vulnerabilities out there but even the most common code examples we read, copy and even run out there were not safe enough.
Kein system ist sicher!
Before starting I would like to suggest taking a look at CWE-943: Improper Neutralization of Special Elements in Data Query Logic to understand what is NoSQL injection and how it related to MongoDB.
You may have read my level-up advice for development environment setup post, here is another database with docker-compose:
version: '3'
services:
database:
image: mongo
ports:
- "27017:27017"
Run MongoDB without authentication, only for experimental purposes, please activate authentication for production!
I will use MongoDB Compass GUI to show a little bit about MongoDB queries.
I have a document - not a table in NoSQL we call it a document not the same but closest naming - called users
.
This is how it looks like as I am not filtering any data:
I would like to filter Jane Smith with email and the query will be:{ $where: "this.email == 'janesmith@example.com'" }
.
So far:
We know how to write a MongoDB Query.
We know how to filter documents.
As you can see from the query there is a comparison and when the expression for the MongoDB operator $where
evaluates to true
the data will be accessed.
We will be replacing the email address of Jane Smith with: '||'true
to escape from the original query and force every $where
the operator is to be calculated as true
.
The first single quote will create an empty string, which forces the email address to be compared to an empty string and will be evaluated as false
. We also concatenated Logical OR and 'true'
where the final expression will fallback to false || true
.
{ $where: "this.email == 'janesmith@example.com'" }
{ $where: "this.email == ''||'true'" }
When you run the injected email value, you will obtain all data from the sample.users
document as shown below:
So far I showed how to exploit a basic MongoDB query, you should know that this is not the only way to exploit it. For this post, I will be focusing on this boolean-based injection payload.
Let's create a MongoDB-Express application to have a real-life example:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const mongoURL = 'mongodb://localhost:27017/sample';
const userSchema = new mongoose.Schema({
name: String,
email: String,
admin: Boolean
});
const User = mongoose.model('User', userSchema);
async function connectToDatabase() {
try {
await mongoose.connect(mongoURL, {
useNewUrlParser: true,
useUnifiedTopology: true
});
console.log('Connected to MongoDB');
} catch (err) {
console.error('Failed to connect to MongoDB:', err);
process.exit(1);
}
}
app.get('/', (req, res) => {
res.send('Running MongoDB with Node.js');
});
app.get('/users', async (req, res) => {
await connectToDatabase();
const email = req.query.email;
try {
const query = { $where: `this.email == '${email}'` };
const users = await User.find(query);
if ( users.length === 0 ) {
res.status(404).json({ error: 'User not found' });
return;
}
res.status(200).json(users);
} catch (err) {
console.error('Failed to retrieve users:', err.message);
console.group('Stack trace');
console.error(err);
console.groupEnd();
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
For this application we will have an endpoint:
http://localhost:3000/users?email=janesmith@example.com will return "User not found" error.
http://localhost:3000/users?email=%27%7C%7C%27true will return all of the data from users document
The decoded query value %27%7C%7C%27true
is equal to '||'true
. Browsers will decode automatically.
Let's focus on the code:
We are assigning email variable with this line:const email = req.query.email;
Here is the first mistake most developers do. We are getting data from outside of the application, users, customers, frontend whatever you call it. We should sanitize the data to be sure it is ready to be used.
There are some libraries to sanitize the data, for Express we can use mongo-sanitize. Basically, we need to escape from the special characters before executing database queries. If we sanitize the boolean injection payload the query and the result will be like:
Here is another suggestion for this case lets take a look at query construction:
const query = { $where: `this.email == '${email}'` };
We are using single quotes for email value and it results in queries below:
{ $where: "this.email == 'janesmith@example.com'" }
{ $where: "this.email == ''||'true'" }
However, we could escape by using backticks to avoid injection payloads:
{ $where: "this.email == `janesmith@example.com`" }
{ $where: "this.email == `'||'true`" }
To conclude we are sanitizing inputs and using default query methods advised to avoid injections.
const email = sanitize(req.query.email); // Sanitize input to avoid NoSQL injection
try {
const users = await User.find({ email }); // use default query options to avoid NoSQL injection
if ( users.length === 0 ) {
res.status(404).json({ error: 'User not found' });
return;
}
res.status(200).json(users);
} catch (err) {
I wanted to draw attention to the importance of writing secure code, as it is susceptible to vulnerabilities. Vulnerabilities arise from the improper neutralization of special elements in data query logic. Attackers can exploit this weakness to manipulate queries, modify selection criteria, append unauthorized commands, or obtain unintended results. To mitigate the risk, developers must focus on implementing rigorous input validation and sanitization techniques for No/SQL Injections. By adopting secure coding practices, such as parameterized queries and proper input handling, developers can fortify MongoDB applications and protect against NoSQL injection attacks.
Thanks for reading so far! Thanks, Sven for exploiting!