Skip to main content

NoSQL injection

Need

Prevention of NoSQL injection attacks

Context

  • Usage of Dart for building cross-platform mobile, web, and desktop applications
  • Usage of Shelf for managing database interactions and data modeling
  • Usage of shelf_router for routing and handling HTTP requests in a Shelf application
  • Usage of mongo_dart for interacting with MongoDB in Dart

Description

Non compliant code

import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_router/shelf_router.dart';
import 'package:mongo_dart/mongo_dart.dart';

class UserController {
final Db db;

UserController(this.db);

Router get router {
final router = Router();

router.post('/login', (shelf.Request request) async {
final params = await request.readAsString();
final username = params['username'];
final password = params['password'];

final userCollection = db.collection('users');
final user = await userCollection.findOne({
'username': username,
'password': password,
});

if (user == null) {
return shelf.Response.forbidden('Invalid username or password');
} else {
return shelf.Response.ok('Login successful');
}
});

return router;
}
}

The above Dart code shows a backend application using the shelf framework and mongo_dart package to interact with a MongoDB database. The UserController class has a router that handles a POST request to the '/login' endpoint.

The vulnerability lies in this part of the code:

final username = params['username'];
final password = params['password'];

final userCollection = db.collection('users');
final user = await userCollection.findOne({
'username': username,
'password': password,
});

Here, the code is reading the 'username' and 'password' directly from the request parameters and using them to create a MongoDB query. This is a security vulnerability known as NoSQL Injection.

An attacker can send a malicious request with special characters or scripts in the 'username' or 'password' fields that can alter the query's behavior, potentially gaining unauthorized access or performing other harmful actions.

The code does not validate or sanitize the input before including it in the query, making it vulnerable to NoSQL Injection attacks.

Steps

  • Validate the user inputs before using them in the NoSQL query. This can be done by checking if the input matches a certain pattern or by using a list of allowed characters.
  • Use parameterized queries or prepared statements if the NoSQL database supports them. This way, the database can distinguish between the query code and the data.
  • Implement a strong password hashing mechanism. Storing passwords in plain text in the database is a security risk. Use a strong cryptographic hash function to store the passwords.
  • Limit the privileges of the database account used by the application. It should only have the necessary permissions to perform its tasks.
  • Implement an error handling mechanism that does not reveal details about the database or the structure of the query. This information can be used by an attacker to refine their injection attacks.

Compliant code

import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf_router/shelf_router.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:crypto/crypto.dart';
import 'dart:convert'; // for the utf8.encode method

class UserController {
final Db db;

UserController(this.db);

Router get router {
final router = Router();

router.post('/login', (shelf.Request request) async {
final params = await request.readAsString();
final username = params['username'];
final password = params['password'];

// Validate the inputs
if (!isValidInput(username) || !isValidInput(password)) {
return shelf.Response.forbidden('Invalid input');
}

// Hash the password
var bytes = utf8.encode(password); // data being hashed
var digest = sha256.convert(bytes);

final userCollection = db.collection('users');
final user = await userCollection.findOne({
'username': username,
'password': digest.toString(),
});

if (user == null) {
return shelf.Response.forbidden('Invalid username or password');
} else {
return shelf.Response.ok('Login successful');
}
});

return router;
}

bool isValidInput(String input) {
// Define the rules for valid input
return input != null && input.length > 0;
}
}

The updated code includes several changes to mitigate the risk of NoSQL injection:

  1. Input Validation: The isValidInput function is used to validate the username and password before they are used in the NoSQL query. This function currently checks if the input is not null and if it has a length greater than 0. Depending on the requirements of your application, you might want to add more checks (e.g., checking if the input matches a certain pattern or if it only contains allowed characters).

  2. Password Hashing: Instead of storing the passwords in plain text in the database, the code now uses the SHA-256 hash function from the crypto package to hash the passwords. This way, even if an attacker manages to inject a NoSQL query and retrieve the data from the database, they will only get the hashed passwords, which are useless without the original password.

  3. Parameterized Queries: The code uses parameterized queries to separate the query code from the data. This way, the database can distinguish between the two and prevent injection attacks.

  4. Error Handling: The code returns a generic 'Invalid input' message if the input validation fails. This way, the application does not reveal any details about the database or the structure of the query, which could be used by an attacker to refine their injection attacks.

  5. Limited Database Privileges: Although not shown in the code, it is recommended to limit the privileges of the database account used by the application. It should only have the necessary permissions to perform its tasks.

References