This article explains what is No-SQL Injection attack, how usually attackers make it and, how we can prevent it.
The attack is called SQL Injection but the same concept applies to No-SQL databases too.
Why it is important to know (No-)SQL Injection?
By taking a look at HackerOne Top Ten Vulnerabilities we can quickly understand the importance of SQL Injection and why we should care about it.
There are two main reasons that make (No-)SQL Injection very tempting for attackers:
- Abundance of (No-)SQL Injection vulnerabilities
- Database is a very attractive target since it contains critical data
What is a (No-)SQL Injection?
What we do on the backend side is mostly based on the data that comes from the request(user). An example is when a user does Login. So we have to get the email of the user and check if it exists and matches the password. We store data in the database and as a result, we have to make a query to the database to find the email. So the Email that the User gave to us is part of the Query. This is where the attacker can do some tricks to extract or manipulate data by giving us something other than a simple Email. This is how an SQL Injection is done.
The Intention of (No-)SQL Injection is access to the database or parts of the database that it should not have.
Examples of No-SQL Injection Attacks
No-SQL Injection(MongoDB)
In the first example, we want to work on Express app that uses MongoDB Native Driver. Why anyone might want to use the native driver? Because in terms of performance, it is the best!(As you can see in the chart below)
import { MongoClient } from "mongodb";
import { Router } from "express";
const url = "mongodb://localhost:27017";
const client = new MongoClient(url);
const dbName = "raw-mongodb";
export const userMongoDBRouter = Router();
userMongoDBRouter.post("/login", async (req, res) => {
const { email, password } = req.body;
await client.connect();
const db = client.db(dbName);
const collection = db.collection("users");
const user = await collection.findOne({
email,
password: hashPassword(password)
});
if (!user) {
res.status(401).send("Invalid credentials");
return;
} else {
res.json({ message: "Login successful" });
}
});
userMongoDBRouter.post("/register", async (req, res) => {
const { email, password } = req.body;
await client.connect();
const db = client.db(dbName);
const collection = db.collection("users");
const user = await collection.insertOne({
email,
password: hashPassword(password)
});
res.json({ message: "User created", user });
});
const hashPassword = (password: string) => {
return password;
};
We have a login route that does not have Validation and passes email and password directly to MongoDB Driver.
First, letβs create a user with the register route and check the Login logic:
Create a Valid User with Password and Email
Now we can login with this valid user that we just created but If we use an invalid user, obviously we cannot Login:
Failed Login with User: Attacker@gmail.com
How to do the No-SQL Injection
We know that MongoDB supports regex with the operator $regex so we can pass this operator instead of a String and use a regex that accepts anything ,*:
{
"email": {
"$regex": ".*"
},
"password": {
"$regex": ".*"
}
}
As you can see in the picture we were able to Login without having a user:
Using Regex Operator to Bypass MongoDB findOne
This was done with MongoDB native driver. Using other ODMβs like Mongoose will not solve this issue:
userMongooseRouter.post("/login", async (req, res) => {
const { email, password } = req.body;
await mongoose.connect(url, { dbName });
const db = mongoose.connection;
const collection = db.collection("users");
// mongoose enable debug mode
mongoose.set("debug", true);
const user = await collection.findOne({
email,
password
});
if (!user) {
res.status(401).send("Invalid credentials");
return;
} else {
res.json({ message: "Login successful" });
}
});
Mongoose is behaving the way we expect. We can bypass login by using $regex .
Bypassing Login when using Mongoose
Is it the responsibility of ODM to Prevent (No-)SQL Injections?
I was mad at my ODM(Mongoose) at first but after a little bit of thinking I realized that Mongoose or Prisma are just libraries for easier database interactions. How could they distinguish that it is you that wants to use the regex or an attacker?
Long story short: It is not the responsibility of ODM or ORM and even if some of them are doing it you cannot be so sure that your app is safe against Injections(Sequalize Security Issue that resulted in SQL Injection).
How to Prevent No-SQL Injection
Avoid Passing Request Objects Directly To ODM or ORM Functions
The worst thing we can do is to pass something like req.body or req.query directly to our ODM/ORM functions:
const user = await collection.findOne(req.body); // Bad Practice
The least we can do is to use the specific fields
const user = await collection.findOne({ userId: req.body.id });
Use Input Validation
Using something like Zod, Yup, and other options.
Example of validating with Zod:
const loginValidator = z.object({
email: z.string().email(),
password: z.string()
});
By using objectStrict it will return an error if someone tries to add extra fields.
const loginValidator = z.strictObject({
email: z.string().email(),
password: z.string()
});
userMongooseRouter.post("/login", async (req, res) => {
const result = loginValidator.safeParse(req.body);
if (!result.success) {
res.status(401).send("Invalid credentials");
return;
}
await mongoose.connect(url, { dbName });
const db = mongoose.connection;
const collection = db.collection("users");
// mongoose enable debug mode
mongoose.set("debug", true);
const user = await collection.findOne(result.data);
if (!user) {
res.status(401).send("Invalid credentials");
return;
} else {
res.json({ message: "Login successful", user: user });
}
});
Now sending a request like this will result in an error:
Sending Extra Field in the Body of Request
Sanitize User Inputs and Filters
Hopefully, Mongoose 6 introduced a sanitizer that you can use: βSanitizes query filters against query selector injection attacks by wrapping any nested objects that have a property whose name starts with $ in a $eq.β
const obj = { username: 'val', pwd: { $ne: null } };
sanitizeFilter(obj);
obj; // { username: 'val', pwd: { $eq: { $ne: null } } });
By wrapping phrases in $eq it prevents injections that are trying to get more data.
If we are not using Mongoose there are other options to consider:
express-mongo-sanitize is a library that checks req.body, req.params, req.query and req.headers for prohibited characters.
Another option is mongo-sanitize that strip out any keys which start with $ in the input.
Conclusion
We know that (No-)SQL Injection is important and common since the target is the database. We need to do some simple steps to prevent this attack: 1- Avoid passing req objects directly, use strong validation, and in the end use sanitizers.
These other articles might be also interesting for you:
- Prevent CSRF Attacks in Node.JS application
- Prevent Parameter Pollution in Node.JS
- How to prevent SSRF attacks in Node.js
- Prevent Brute Force Attacks in Node.JS
- SQL Injection Prevention - OWASP Cheat Sheet Series
Level Up Coding
Thanks for being a part of our community! Before you go:
- π Clap for the story and follow the author π
- π° View more content in the Level Up Coding publication
- π° Free coding interview course β View Course
- π Follow us: Twitter | LinkedIn | Newsletter
ππ Join the Level Up talent collective and find an amazing job
Top comments (0)