DEV Community

Poorshad Shaddel
Poorshad Shaddel

Posted on • Originally published at levelup.gitconnected.com on

How to Prevent No-SQL Injection in Node.js

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.


SQL or No-SQL Injection

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.

SQL Injection Rank in HackerOne Bonty(Rank 7th)
SQL Injection Bounty ranking

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)


Mongoose vs MongoDB Driver

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;
};
Enter fullscreen mode Exit fullscreen mode

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": ".*" 
  }
}
Enter fullscreen mode Exit fullscreen mode

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" });
  }
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The least we can do is to use the specific fields

const user = await collection.findOne({ userId: req.body.id });
Enter fullscreen mode Exit fullscreen mode

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()
});
Enter fullscreen mode Exit fullscreen mode

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 });
  }
});
Enter fullscreen mode Exit fullscreen mode

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 } } });
Enter fullscreen mode Exit fullscreen mode

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:

Level Up Coding

Thanks for being a part of our community! Before you go:

πŸš€πŸ‘‰ Join the Level Up talent collective and find an amazing job


Top comments (0)