HIGH cache poisoningactixfirestore

Cache Poisoning in Actix with Firestore

Cache Poisoning in Actix with Firestore — how this specific combination creates or exposes the vulnerability

Cache poisoning in an Actix web service that uses Firestore as a backend data store occurs when an attacker manipulates cache keys or cacheable responses such that malicious or unintended data is served to other users. Because Actix applications often cache Firestore query results to reduce read latency and costs, unsafe construction of cache keys or failure to validate cached content can cause one user’s data to be served to another, or cause stale or modified Firestore documents to be trusted implicitly.

One common pattern is to derive cache keys directly from user-supplied parameters without normalizing or canonicalizing them. For example, an Actix handler might build a cache key like format!("user:{}:profile", user_id) where user_id comes from a query parameter or header. If the application does not enforce strict schema validation and type conversion (e.g., treating a numeric ID as a string), an attacker can supply a crafted value that results in a cache key collision across different logical data sets. Because Firestore document paths are sensitive to exact string matching, a malformed or ambiguous key may map to a document the attacker does not own but that is cached under the same key for a different tenant or context.

Another vector arises from caching aggregated or reflected Firestore query results that include dynamic fields controlled by the client. If an Actix endpoint caches the raw Firestore DocumentSnapshot or query result and later embeds it into responses without stripping or re-validating sensitive fields, an attacker may inject malicious metadata (such as modified TTL-like fields or client-controlled cache-control headers) that affect how the cached entry is reused. Because Firestore does not inherently understand HTTP caching semantics, the Actix layer must enforce its own validity rules; otherwise, poisoned cache entries can persist across requests and users.

Additionally, if the Actix service uses Firestore’s real-time listeners or snapshot diffing to populate cache, an attacker who can influence the query filters (e.g., via wildcard parameters) may cause the cache to be updated with malicious data. For instance, an unsafe Firestore query constructed from concatenated strings can be tricked into watching or returning documents outside the intended scope, and the resulting changes can be reflected in the cache in real time, leading to widespread cache poisoning across clients.

Firestore-Specific Remediation in Actix — concrete code fixes

To mitigate cache poisoning when integrating Firestore with Actix, enforce strict input validation, canonicalize cache keys, and isolate cache namespaces per tenant or user context. Avoid directly using raw user input in cache keys or Firestore query constraints. Below are concrete, idiomatic examples for Actix with Firestore in Rust.

1. Canonicalize and scope cache keys

Use a deterministic, type-safe representation for cache keys and scope them by tenant or user ID. Prefer hashing structured data rather than concatenating raw strings.

use sha2::{Sha256, Digest};
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

fn canonical_user_key(user_id: &str, tenant: &str) -> String {
    let mut hasher = DefaultHasher::new();
    format!("{}:{}", tenant, user_id).hash(&mut hasher);
    let hash = hasher.finish();
    format!("tenant:{}:user:{}", tenant, hash)
}

// Example usage in an Actix handler
async fn get_profile(
    query: web::Query>,
    firestore: web::Data,
) -> Result {
    let tenant = "acme-tenant";
    let user_id = query.get("user_id").ok_or_else(|| error::ErrorBadRequest("missing user_id"))?;
    if !user_id.chars().all(|c| c.is_ascii_alphanumeric()) {
        return Err(error::ErrorBadRequest("invalid user_id"));
    }
    let cache_key = canonical_user_key(user_id, tenant);
    // Proceed to Firestore read or cache lookup using cache_key
    Ok(HttpResponse::Ok().finish())
}

2. Validate and parameterize Firestore queries

Always use Firestore’s built-in parameter binding and avoid string interpolation. Ensure query filters explicitly constrain scope to the tenant and user.

use google_cloud_firestore::client::Client;
use google_cloud_firestore::firestore::StructuredQuery::CollectionGroupFilter;
use google_cloud_firestore::firestore::StructuredQuery::Filter as QueryFilter;
use google_cloud_firestore::firestore::structured_query::Filter as StructuredFilter;
use google_cloud_firestore::firestore::value::Value as FirestoreValue;
use google_cloud_firestore::firestore::StructuredQuery;

async fn fetch_user_data(client: &Client, tenant: &str, user_id: &str) -> Result {
    let doc_path = format!("tenants/{}/users/{}", tenant, user_id);
    let doc_ref = client.doc(&doc_path);
    let snapshot = doc_ref.get().await?;
    if !snapshot.exists() {
        return Err(Error::NotFound("document does not exist"));
    }
    // Verify tenant field matches to prevent horizontal escalation
    let data: HashMap = snapshot.data().ok_or_else(|| Error::Internal("no data"))?;
    if data.get("tenant") != Some(&FirestoreValue::string_value(tenant.to_string())) {
        return Err(Error::Forbidden("tenant mismatch"));
    }
    Ok(snapshot)
}

3. Normalize and sanitize cached responses

Strip or re-validate sensitive fields before caching query results. Do not cache entries with mutable fields that can be influenced by the client.

use serde_json::{json, Value};

fn sanitize_firestore_doc(mut doc: Value) -> Value {
    let obj = doc.as_object_mut().expect("document should be an object");
    obj.remove("internal_metadata");
    obj.remove("admin_only");
    // Ensure tenant is canonical and matches expected scope
    if let Some(tenant) = obj.get("tenant") {
        if !tenant.is_string() {
            return json!({"error": "invalid tenant"});
        }
    }
    doc
}

// In handler before caching
let raw = firestore.get_user_data(&client, tenant, user_id).await?;
let safe = sanitize_firestore_doc(raw.into_json()?);
cache.insert(cache_key, safe);

4. Isolate cache namespaces and enforce strict TTL

Use tenant- and user-specific cache namespaces and configure short, bounded TTLs to limit the window of poisoned entries. Combine with per-request revalidation against Firestore when sensitive fields are present.

5. Avoid reflective query construction

Never build Firestore query filters by concatenating user input. Use strongly typed structures or explicit allow-lists for field names and values.

// UNSAFE: building filters from user input
// let filter_str = format!("{} >= {}", field_name, user_value);

// SAFE: explicit, controlled filter
let structured_query = StructuredQuery {
    from: vec![CollectionGroupFilter { collection_id: "users".to_string() }],
    where_field: Some(Box::new(StructuredFilter {
        field_filter: Some(Box::new(google_cloud_firestore::firestore::field_filter::FieldFilter {
            field: google_cloud_firestore::firestore::FieldReference {
                field_path: "tenant".to_string(),
            },
            op: google_cloud_firestore::firestore::structured_query::FieldFilterOp::Equal as i32,
            value: Some(FirestoreValue::string_value(tenant.to_string())),
        })),
    })),
    ..Default::default()
};

By combining strict input validation, canonical cache keys, parameterized queries, and response sanitization, you reduce the risk of cache poisoning in Actix applications that rely on Firestore as a backend.

Frequently Asked Questions

Can cache poisoning in Actix with Firestore lead to cross-tenant data exposure?
Yes. If cache keys are derived from insufficiently validated user input and lack tenant isolation, an attacker can cause cached data from one tenant to be served to another, potentially exposing sensitive Firestore documents.
Does Firestore protect against cache poisoning on its own?
No. Firestore provides authentication, authorization, and data storage but does not enforce HTTP caching semantics or prevent application-layer cache poisoning. The Actix service must implement strict cache key design, input validation, and response sanitization.