NoSQL injection
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