Replay Attack in Actix with Firestore
Replay Attack in Actix with Firestore — how this specific combination creates or exposes the vulnerability
A replay attack in the context of an Actix-web service backed by Firestore occurs when an attacker intercepts a valid request—typically including an authentication token or an operation identifier—and re-sends it to the API to achieve unauthorized effects. Because Actix is a Rust web framework that often uses JWTs, API keys, or session cookies for authorization, and because Firestore is a cloud-hosted NoSQL database that may enforce its own access controls at the document or collection level, the combined stack can inadvertently allow duplicated requests to succeed if neither side enforces strict replay protection.
The vulnerability surface arises when idempotency is not enforced at the application layer. For example, an HTTP POST to create or update a Firestore document may carry a client-generated request ID. If Actix does not validate that this ID has been processed before, and Firestore does not enforce a uniqueness constraint or conditional write (e.g., via document ID uniqueness or a transaction guard), the same request can be applied multiple times. This might result in duplicate financial transactions, repeated state changes, or privilege escalation if the request promotes a user role. Firestore’s server-side timestamps and last-write-wins semantics can exacerbate the issue: an older request with a newer timestamp may overwrite more recent legitimate data if replayed after an unrelated update.
Common root causes include missing nonce or timestamp checks in Actix handlers, lack of optimistic concurrency control using Firestore document versions or transactions, and insufficient transport-layer protections that enable request interception. Even when TLS is used, captured requests can be replayed before token expiration. Insecure default Firestore rules that allow broad write access can further reduce the cost of replay for an attacker. Because middleBrick scans the unauthenticated attack surface, it can detect endpoints where replay protections are absent and highlight the risk in the context of the API’s authentication and authorization checks.
Firestore-Specific Remediation in Actix — concrete code fixes
To mitigate replay attacks in an Actix service using Firestore, implement idempotency keys, strict timestamp validation, and conditional writes. Ensure each request includes a unique client-supplied idempotency token that Actix checks against a cache or Firestore collection of processed tokens with a short TTL.
Example: an Actix handler using the Firestore Rust SDK with idempotency and transaction guard.
use actix_web::{post, web, HttpResponse, Result};
use google_cloud_firestore::client::Client;
use google_cloud_firestore::firestore::v1::document::Document;
use google_cloud_firestore::firestore::v1::StructuredQuery;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
async fn is_idempotent(client: &Client, idempotency_key: &str) -> bool {
// Check a dedicated Firestore collection for processed keys (with TTL policy)
let doc_ref = client.collection("idempotency").doc(idempotency_key);
doc_ref.get().await.map(|doc| doc.exists()).unwrap_or(false)
}
async fn record_idempotent(client: &Client, idempotency_key: &str) -> Result<(), Box> {
let doc_ref = client.collection("idempotency").doc(idempotency_key);
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
// Store with a server-side timestamp to avoid client clock skew issues
doc_ref.set(&[("created_at", google_cloud_firestore::firestore::v1::Value {
timestamp_value: Some(now),
..Default::default()
})], None).await?;
Ok(())
}
#[post("/account/deposit")]
async fn deposit(
client: web::Data>,
body: web::Json,
) -> Result {
let req = body.into_inner();
// Require idempotency key in header
let id_key = match req.headers.get("Idempotency-Key") {
Some(k) => k,
None => return Ok(HttpResponse::BadRequest().body("missing idempotency key")),
};
if is_idempotent(&client, &id_key).await {
return Ok(HttpResponse::Conflict().body("duplicate request"));
}
// Use a transaction to ensure atomic check-and-write on target document
let doc_ref = client.collection("accounts").doc(&req.account_id);
let transaction = client.transaction().await?;
let snapshot = transaction.get(&doc_ref).await?;
let current: i64 = snapshot.get("balance").unwrap_or(0);
let new_balance = current + req.amount;
// Conditional update using the transaction; if document changed since read, abort
transaction.set(&doc_ref, &[("balance", google_cloud_firestore::firestore::v1::Value {
integer_value: Some(new_balance),
..Default::default()
})]);
transaction.commit().await?;
// Record idempotency after successful transaction
record_idempotent(&client, &id_key).await?;
Ok(HttpResponse::Ok().json(serde_json::json!({ "balance": new_balance })))
}
In this example, the handler first checks an idempotency collection in Firestore to reject duplicates. It then uses a Firestore transaction to read and update the account balance conditionally, preventing race conditions. Finally, it records the processed idempotency key to ensure subsequent replays are rejected. Complement these patterns with HTTPS enforcement, short token lifetimes, and rate limiting to reduce interception risk.