Cache Poisoning in Axum with Firestore
Cache Poisoning in Axum with Firestore — how this specific combination creates or exposes the vulnerability
Cache poisoning in an Axum service that uses Google Cloud Firestore occurs when an attacker manipulates data or cache keys so that malicious or incorrect data is served to subsequent requests. Because Axum is a Rust web framework and Firestore is a distributed NoSQL datastore, the risk centers on how responses are cached (in-memory, reverse proxy, or client caches) and how Firestore queries are constructed and parameterized.
Consider an endpoint that retrieves user profiles by a numeric user ID and caches the Firestore document read result. If the cache key is derived from raw user input (e.g., format!("user:{}", user_id)) without strict validation or normalization, an attacker can supply crafted input such as 123 and 123?profile_variant=evil that maps to the same or a different cache entry depending on how the cache normalizes keys. A vulnerable Axum handler might look like this:
async fn get_user_profile(
user_id: String,
cache: Extension<Cache>,
) -> Result<Json<UserProfile>, (StatusCode, String)> {
let cache_key = format!("user:{}", user_id);
if let Some(cached) = cache.get(&cache_key) {
return Ok(Json(cached));
}
let db = FirestoreDb::new();
let profile = db.collection("profiles").doc(user_id).get().await?;
cache.insert(cache_key, profile.clone());
Ok(Json(profile))
}
If user_id is not validated, an attacker can provide values that cause cache collisions or store maliciously altered representations. For example, supplying a user ID containing path-like segments or specially crafted strings may cause the cache to key documents inconsistently, leading to one user seeing another’s data (a BOLA/IDOR-like effect enabled by cache behavior). Firestore does not introduce caching itself, but the combination of Axum application-layer caching and Firestore document reads can amplify risks when cache keys are not deterministic and scoped correctly.
Another scenario involves query parameter pollution affecting Firestore queries used to populate cache entries. If an endpoint constructs a Firestore query using a map of parameters derived from user-supplied inputs without strict allowlisting, attackers can inject additional query parameters that change the result set used to populate or invalidate the cache. This can lead to cache entries being populated with unintended subsets of data or with data that includes sensitive fields unintentionally exposed due to missing field-level security in the query. For instance, an endpoint that builds a Firestore query from a JSON object forwarded directly from the request can inadvertently include extra filters that change which documents are cached:
async fn list_items(
query: web::Query<HashMap<String, String>>,
cache: Extension<Cache>,
) -> Result<Json<Vec<Item>>, (StatusCode, String)> {
let cache_key = format!("items:{:?}", query);
if let Some(cached) = cache.get(&cache_key) {
return Ok(Json(cached));
}
let db = FirestoreDb::new();
let mut coll = db.collection("items");
for (k, v) in query.iter() {
coll = coll.filter(k, FilterOp::Eq, v);
}
let items = coll.get().await?;
cache.insert(cache_key, items.clone());
Ok(Json(items))
}
Here, an attacker can add parameters such as __firestore_override=true or extra field filters that change the cached result set. Because Firestore queries are built dynamically, cache keys that include the full query state may inadvertently encode attacker-influenced data, leading to poisoning via cache key manipulation. This can also interact with Firestore’s indexing and permissioning, where unintended query paths expose data that should not be cached or shared across users.
To summarize, cache poisoning in Axum with Firestore emerges from insecure cache key construction, dynamic query building without allowlists, and insufficient input validation. The risks are not in Firestore itself but in how Axum orchestrates reads, caching, and query construction. Mitigations require strict input validation, canonical cache keys, and disciplined query building that avoids injecting untrusted data into cache or query logic.
Firestore-Specific Remediation in Axum — concrete code fixes
Remediation focuses on canonicalizing inputs, parameterizing queries safely, and ensuring cache keys are deterministic and scoped to user and query context. Below are concrete Axum handler examples that address the two scenarios above.
1) Validate and normalize user IDs before using them in cache keys and Firestore document paths.
Use a strongly typed ID type and parse/validate before constructing cache keys or document references:
use axum::{Extension, Json};
use std::net::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct UserProfile { /* fields */ }
async fn get_user_profile(
Json(payload): Json<UserRequest>,
cache: Extension<Cache>,
) -> Result<Json<UserProfile>, (StatusCode, String)> {
let user_id = payload.user_id;
// Validate: positive integer within expected range
if user_id == 0 || user_id > 1_000_000 {
return Err((StatusCode::BAD_REQUEST, "Invalid user ID".into()));
}
// Canonical cache key scoped to the operation and user
let cache_key = format!("v1:profile:uid:{}", user_id);
if let Some(cached) = cache.get(&cache_key) {
return Ok(Json(cached));
}
let db = FirestoreDb::new();
// Use a strongly-typed document ID; avoid raw concatenation
let doc_ref = db.collection("profiles").doc(user_id.to_string());
let profile = doc_ref.get().await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
cache.insert(cache_key, profile.clone());
Ok(Json(profile))
}
2) Build Firestore queries safely using allowlists and avoid dynamic injection into query construction.
Do not forward arbitrary query parameters into Firestore. Instead, accept only known fields and map them explicitly:
async fn list_items(
Extension(cache): Extension<Cache>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Json<Vec<Item>>, (StatusCode, String)> {
// Allowlist known filterable fields
const ALLOWED: &[&str] = &["category", "status"];
let mut query_builder = db.collection("items");
for key in params.keys() {
if !ALLOWED.contains(&key.as_str()) {
return Err((StatusCode::BAD_REQUEST, "Unsupported filter".into()));
}
}
if let Some(category) = params.get("category") {
query_builder = query_builder.filter("category", FilterOp::Eq, category);
}
if let Some(status) = params.get("status") {
query_builder = query_builder.filter("status", FilterOp::Eq, status);
}
let cache_key = format!("items:category:{}:status:{}",
params.get("category").unwrap_or("any"),
params.get("status").unwrap_or("any")
);
if let Some(cached) = cache.get(&cache_key) {
return Ok(Json(cached));
}
let items = query_builder.get().await.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
cache.insert(cache_key, items.clone());
Ok(Json(items))
}
3) Treat Firestore security rules as part of your defense-in-depth, but do not rely on them for cache correctness.
Ensure Firestore rules restrict reads to the user’s own data where applicable, but also enforce strict input validation in Axum. Rules are complementary, not a substitute for canonical cache keys and validated queries.
By combining typed IDs, allowlisted query parameters, and deterministic cache keys, you reduce the surface for cache poisoning when using Axum with Firestore.