Nosql Injection in Axum
How NoSQL Injection Manifests in Axum
NoSQL injection in Axum applications typically occurs when user-supplied JSON payloads are directly converted into query filters for a NoSQL database like MongoDB without proper validation or parameterization. Axum's ergonomic extractors, such as Json<T>, can inadvertently encourage unsafe patterns if the extracted data is used to construct dynamic queries.
A common vulnerable pattern involves accepting a generic serde_json::Value or bson::Document from the request body and passing it directly to a MongoDB find() operation. For example:
use axum::{Json, extract::State};
use mongodb::{bson::doc, Client};
use serde::Deserialize;
use std::sync::Arc;
type Db = Arc<Client>;
#[derive(Deserialize)]
struct SearchRequest {
filter: serde_json::Value, // User-controlled JSON object
}
async fn search(State(db): State<Db>, Json(body): Json<SearchRequest>) -> Result {
let collection = db.database("mydb").collection("mycoll");
let filter: mongodb::bson::Document = match bson::to_bson(&body.filter) {
Ok(bson::Value::Document(doc)) => doc,
_ => return Err((StatusCode::BAD_REQUEST, "Invalid filter".to_string())),
};
// VULNERABLE: user-supplied document used directly as filter
let results = collection.find(filter, None).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// ... process results
Ok("OK".to_string())
} An attacker can exploit this by sending a payload like {"filter": {"$where": "function() { return true; }"}}. The $where operator allows execution of JavaScript on the database, potentially bypassing authentication or exfiltrating data. Other dangerous operators include $ne, $gt, $regex, and $expr.
Even when using typed structs, developers might mistakenly interpolate user input into a doc! macro with string values that later get interpreted as documents. For instance, if a field expects a string but an attacker submits {"username": {"$ne": null}}, and the code does doc!{"username": &body.username}, the value becomes a BSON string, not an operator—so this specific case is safe. The real danger lies in allowing arbitrary document structures from the user.
Axum-Specific Detection
To detect NoSQL injection vulnerabilities in Axum code, review any endpoint that accepts JSON and interacts with a NoSQL database. Look for these red flags:
- Use of
serde_json::Value,bson::Document, orHashMap<String, Value>as extractor types without subsequent validation. - Direct conversion of request JSON to a BSON document via
bson::to_bson()or similar, then passing it to database operations. - Construction of queries by merging user input into
doc!orbson::Documentwithout sanitizing keys or values.
Dynamic analysis via black-box scanning is equally effective. middleBrick's Input Validation check actively probes for NoSQL injection by sending payloads containing MongoDB operators and observing response anomalies. For the vulnerable endpoint above, middleBrick would test payloads like:
POST /search
{
"filter": {
"$where": "function() { return true; }"
}
}If the response returns all documents (instead of a filtered set) or exhibits error messages indicating query parsing issues, middleBrick flags it as a potential NoSQL injection. The scanner also tests for boolean-based injection ($ne: null), regex injection, and operator-based bypasses. This runtime testing is crucial because static analysis might miss logical flaws in how filters are built.
Axum-Specific Remediation
Remediate NoSQL injection in Axum by strictly controlling the structure of query filters. Never trust user-supplied documents. Instead, define explicit request structs and build filters using known fields and operators. Axum's extractors work seamlessly with serde to enforce schema validation.
Safe Pattern 1: Typed Request Structs
Define a struct that only includes allowed filter parameters. Use Option<T> for optional criteria and construct the filter manually:
use axum::{Json, extract::State};
use mongodb::bson::{doc, Bson};
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchRequest {
name: Option<String>,
min_age: Option<u32>,
exact_email: Option<String>,
}
async fn search(State(db): State<Db>, Json(body): Json<SearchRequest>) -> Result<String, (StatusCode, String)> {
let collection = db.database("mydb").collection("users");
let mut filter = doc! {};
if let Some(name) = body.name {
// Safe: we control the key "name" and the value is a string
filter.insert("name", name);
}
if let Some(min_age) = body.min_age {
// Safe: we explicitly use the $gte operator with a numeric value
filter.insert("age", doc! { "$gte": min_age });
}
if let Some(email) = body.exact_email {
filter.insert("email", email);
}
// filter is now built only from our validated struct fields
let results = collection.find(filter, None).await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// ...
Ok("OK".to_string())
}Safe Pattern 2: Reject Unknown Fields
Add #[serde(deny_unknown_fields)] to your request struct to ensure no extra JSON keys are accepted. This prevents an attacker from injecting arbitrary operators via unexpected fields.
Safe Pattern 3: Validate Input Values
If you must accept a map of field-value pairs (e.g., for a generic search), validate each key and value. Reject any key starting with $ and ensure values are of expected types (strings, numbers, booleans). Use the validator crate for custom validation:
use validator::{Validate, ValidationError};
#[derive(Deserialize, Validate)]
struct GenericSearchRequest {
#[validate(custom = "validate_no_dollar")]
field: String,
value: String,
}
fn validate_no_dollar(field: &str) -> Result<(), ValidationError> {
if field.starts_with('$') {
return Err(ValidationError::new("field_cannot_start_with_dollar"));
}
Ok(())
}However, the safest approach is to avoid dynamic filters entirely and design your API with predefined query parameters. This aligns with the principle of least privilege and makes your API's contract explicit.
Frequently Asked Questions
What is NoSQL injection and why is it a risk in Axum applications?
$where or $ne via user input. In Axum, this often happens when endpoints accept generic JSON payloads and convert them directly into database filters. Successful exploitation can lead to authentication bypass, data exfiltration, or even remote code execution if $where is used. Because Axum's extractors make it easy to accept arbitrary JSON, developers must be vigilant about validating and sanitizing any data that influences database queries.How does middleBrick detect NoSQL injection vulnerabilities in my Axum API?
{"$ne": null} in a login parameter might return a successful response even with invalid credentials, signaling a vulnerability. This runtime testing complements static code review and is part of middleBrick's Input Validation check, one of its 12 parallel security assessments.