Nosql Injection in Actix with Firestore
Nosql Injection in Actix with Firestore — how this specific combination creates or exposes the vulnerability
When building a REST API with Actix-web and a backend datastore like Google Cloud Firestore, improper handling of user input that is used to construct queries can lead to NoSQL Injection. Firestore does not use a traditional SQL syntax, but it does support structured queries over collections and documents. If an attacker can influence query parameters such as field paths, values used in filters, or keys used to retrieve documents, they may be able to read or modify data that should be isolated.
Consider an endpoint that retrieves a user profile by a user-supplied UID. If the server directly uses the input to reference a document without strict validation, an attacker might supply a document ID like users/abc or attempt path traversal such as ../../../other-collection/doc. Even though Firestore enforces security rules, the application layer may still expose unintended data depending on how the client SDK is used server-side. A more subtle injection can occur when input is used to construct field paths or map keys in aggregation or write operations. For example, if an endpoint dynamically builds a map to update a document and uses user input as a key, an attacker could supply keys like __proto__ or other special property names that affect object behavior in JavaScript runtimes or bypass expected validation logic.
The combination of Actix-web request handling and Firestore client usage amplifies risks when developers assume the database will always enforce strict schema boundaries. Firestore security rules can mitigate some risks, but they must be carefully designed to account for dynamic queries built from user input. For instance, rules that allow read access based on document ownership must ensure the document path cannot be overridden by malicious input. Without rigorous input validation and canonicalization, an API built with Actix and Firestore may inadvertently allow horizontal privilege escalation through NoSQL Injection techniques.
Firestore-Specific Remediation in Actix — concrete code fixes
Defending against NoSQL Injection in an Actix service that uses Firestore requires strict input validation, canonical path construction, and disciplined use of the Firestore SDK. Always treat user input as untrusted and avoid directly interpolating it into document paths or query constraints.
Below are concrete, idiomatic examples for Actix-web using the Firestore Rust SDK. The first example shows a vulnerable pattern where user input is used directly to reference a document. The second shows a hardened approach that validates and scopes access.
Vulnerable pattern
use actix_web::{web, HttpResponse};
use google_cloud_firestore::client::Client;
async fn get_profile_vulnerable(client: web::Data, user_id: web::Path) -> HttpResponse {
let doc_path = format!("users/{}", user_id);
let doc = client.get_document(&doc_path).await;
match doc {
Ok(document) => HttpResponse::Ok().json(document),
Err(_) => HttpResponse::NotFound().finish(),
}
}
Hardened remediation
Validate the user_id to ensure it contains only allowed characters, and construct the document reference using a fixed prefix. This prevents path traversal and injection via special keys or delimiters.
use actix_web::{web, HttpResponse};
use google_cloud_firestore::client::Client;
use regex::Regex;
async fn get_profile_hardened(client: web::Data, user_id: web::Path) -> HttpResponse {
// Allow only alphanumeric and underscore, typical for document IDs
let re = Regex::new(r"^[a-zA-Z0-9_-]{1,64}$").unwrap();
if !re.is_match(&user_id) {
return HttpResponse::BadRequest().body("Invalid user ID");
}
let doc_path = format!("users/{}", user_id);
let doc = client.get_document(&doc_path).await;
match doc {
Ok(document) => HttpResponse::Ok().json(document),
Err(_) => HttpResponse::NotFound().finish(),
}
}
For queries that filter on user-supplied values, avoid dynamically building field paths. Instead, use explicit field names and parameterized values. The following example demonstrates a safe query that retrieves documents where a known field matches a validated input.
use google_cloud_firestore::client::Client;
use google_cloud_firestore::firestore::query::Query;
async fn list_items_for_user(client: web::Data, user_id: web::Path) -> HttpResponse {
let re = Regex::new(r"^[a-zA-Z0-9_-]{1,64}$").unwrap();
if !re.is_match(&user_id) {
return HttpResponse::BadRequest().body("Invalid user ID");
}
let query = Query::new()
.collection("items")
.eq("owner_id", &user_id);
let results = client.run_query(query).await;
match results {
Ok(items) => HttpResponse::Ok().json(items),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
When performing updates, avoid using user input as map keys. Use static field names or a controlled mapping. This prevents issues with special property names such as __proto__ or constructor that can affect runtime behavior in downstream processing.
use google_cloud_firestore::client::Client;
use serde_json::json;
async fn update_settings_safe(client: web::Data, user_id: web::Path, settings: web::Json) -> HttpResponse {
let re = Regex::new(r"^[a-zA-Z0-9_-]{1,64}$").unwrap();
if !re.is_match(&user_id) {
return HttpResponse::BadRequest().body("Invalid user ID");
n }
// Use a static field name instead of dynamic keys
let update_data = json!({
"preferences": settings.get("preferences").cloned().unwrap_or(serde_json::Value::Null)
});
let doc_path = format!("users/{}", user_id);
let result = client.update_document(&doc_path, update_data).await;
match result {
Ok(_) => HttpResponse::NoContent().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
In addition to input validation, apply principle of least privilege to the Firestore credentials used by the Actix service. Ensure that the service account or token used by the Firestore client has only the necessary read/write permissions for the collections it needs, reducing the impact of a potential injection or misconfiguration.