logo

Database

Need

Prevention of NoSQL injection by keeping the structure of database queries fixed

Context

• Usage of Rust 1.75+ for building application services

• Usage of the actix-web crate for handling HTTP requests

• Usage of the mongodb crate for database access

• Usage of the argon2 crate for password hashing

Description

1. Non compliant code

use actix_web::{post, web, HttpResponse};
use mongodb::bson::Document;
use mongodb::Collection;

#[post("/login")]
async fn login(
    users: web::Data<Collection<Document>>,
    body: web::Json<Document>,...

The `login` handler below deserializes the JSON request body into a BSON `Document` and uses that document directly as the filter of `find_one`. The caller therefore controls the whole query, operators included. Sending `{"username": "admin", "password": {"$ne": ""}}` matches the administrator account without knowing its password, because `$ne` is evaluated by MongoDB as "not equal to the empty string". Operators such as `$regex`, `$gt` or `$where` let an attacker enumerate users, extract field values character by character, or run expensive queries. The same weakness exists with the `aws-sdk-dynamodb` crate when request data is interpolated with `format!` into `filter_expression`, `key_condition_expression` or `update_expression`, since those strings are parsed by DynamoDB as query syntax.

2. Steps

• Deserialize request bodies into structs with concrete field types instead of `bson::Document` or `serde_json::Value`, so operator objects are rejected before they reach the database.

• Build MongoDB filters with the `doc!` macro and fixed field names; insert request data only as values.

• Reject keys that start with `$` or contain `.` when a dynamic document must be accepted from a caller.

• For `aws-sdk-dynamodb`, keep `filter_expression`, `key_condition_expression` and `update_expression` as constant strings and pass request data through `expression_attribute_values`.

• When a caller selects an attribute name, map it through an allowlist before placing it in `expression_attribute_names`.

• Store password hashes created with Argon2 or bcrypt and verify them in application code, never by matching plaintext passwords in a query.

3. Secure code example

use actix_web::{post, web, HttpResponse};
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use mongodb::bson::{doc, Document};
use mongodb::Collection;
use serde::Deserialize;

#[derive(Deserialize)]
struct LoginRequest {...

The corrected handler deserializes the body into a `LoginRequest` struct with two `String` fields. Serde rejects any request in which `username` or `password` is an object, so values such as `{"$ne": ""}` never reach the database. The filter is written with the `doc!` macro, which fixes its shape: the only thing the caller supplies is the value compared with `username`. The password is no longer part of the query at all. The handler loads the stored Argon2 hash and verifies the submitted password with `PasswordVerifier::verify_password`, so the database never compares plaintext passwords. For DynamoDB, the equivalent fix is to write the expression as a constant string with placeholders (`#name`, `:value`) and pass request data only through `expression_attribute_values`.

References

• 106. NoSQL injection