Password Spraying in Actix with Firestore
Password Spraying in Actix with Firestore — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication-layer attack where a single common password (e.g., Password1) is tried against many accounts. When an Actix web service uses Firestore as its identity store and exposes a login endpoint without adequate rate limiting or lockout, spraying becomes practical. Unlike brute-forcing individual accounts, spraying tests one credential across many user IDs, which can bypass per-account lockouts and stay under simplistic rate thresholds.
An Actix app typically receives a JSON payload like {"email": "[email protected]", "password": "..."}, looks up the document in a Firestore collection (e.g., users), and compares the provided password to a stored hash. If the endpoint returns distinct responses for missing accounts versus authentication failures, an attacker can enumerate valid emails. Combine this with weak password policies and missing multi-factor authentication, and Firestore’s predictable document IDs or indexed fields can make targeted spraying efficient.
Consider an Actix route that queries Firestore without guarding against rapid, low-concurrency requests:
async fn login(Form(payload): Form<LoginPayload>, db: web::Data<FirestoreDb>) -> impl Responder {
let user_doc = db.collection("users").doc(&payload.email).get().await;
match user_doc {
Ok(doc) => {
let stored_hash: String = doc.get("password_hash").unwrap_or_default();
if verify_password(&payload.password, &stored_hash) {
HttpResponse::Ok().finish()
} else {
HttpResponse::Unauthorized().body("Invalid credentials")
}
},
Err(_) => HttpResponse::Unauthorized().body("Invalid credentials"),
}
}
An attacker can iterate over emails and send the same password. If the response time differs (e.g., Firestore read latency for existing vs non-existing docs) or status codes vary subtly, the attacker gains hints. The absence of per-IP or per-account rate limiting, combined with missing CAPTCHA or progressive delays, lets a single attacker-controlled client run a spray without triggering defenses. Since middleBrick tests Authentication and Rate Limiting in parallel, such weaknesses are surfaced as high-severity findings with remediation guidance to enforce uniform error handling and strict rate limits.
Firestore-Specific Remediation in Actix — concrete code fixes
Remediation focuses on making authentication behavior constant-time and rate-aware, regardless of whether the email exists, and adding defense-in-depth controls around Firestore access in Actix.
First, enforce a fixed delay and identical response shape for all login attempts. Use a constant-time password verification routine and avoid branching on document existence:
async fn login_secure(
Form(payload): Form<LoginPayload>,
db: web::Data<FirestoreDb>,
rate_limiter: web::Data<RateLimiter>,
) -> impl Responder {
// Enforce rate limiting before any Firestore work
if !rate_limiter.check(&payload.email, &request_ip) {
return HttpResponse::TooManyRequests().json(json!({"error": "rate limit exceeded"}));
}
// Always fetch a document; if missing, synthesize a dummy hash to keep timing consistent
let user_doc = db.collection("users").doc(&payload.email).get().await;
let stored_hash = match user_doc {
Ok(doc) => doc.get("password_hash").unwrap_or_else(|_| DEFAULT_HASH.to_string()),
Err(_) => DEFAULT_HASH.to_string(),
};
// Constant-time compare to avoid timing leaks
let is_valid = subtle::ConstantTimeEq::eq(
&crypto_hash(&payload.password),
&crypto_hash(&stored_hash),
);
if is_valid.into() {
HttpResponse::Ok().json(json!({"status": "ok"}))
} else {
// Same shape, same status code to prevent enumeration
HttpResponse::Unauthorized().json(json!({"error": "invalid credentials"}))
}
}
Second, add per-identifier and global rate limiting. For example, use a token-bucket or sliding-window store (Redis or in-memory with caution) keyed by email and IP, with configurable thresholds:
struct RateLimiter {
// pseudo: store with TTL
store: Arc<dyn IdentifierLimiter>,
global_limiter: Arc<dyn RateLimiter>,
}
impl RateLimiter {
fn check(&self, email: &str, ip: &str) -> bool {
self.global_limiter.allow(ip) && self.store.allow(email, 5, Duration::from_secs(60))
}
}
Third, harden Firestore usage by avoiding client-supplied paths that could lead to SSRF or IDOR. Validate and normalize emails before using them as document keys, and ensure Firestore security rules (server-side) reject unexpected fields. Prefer parameterized queries over dynamic string concatenation to prevent injection-style confusion in path building.
Finally, instrument your Actix logs without recording raw passwords, and monitor for spikes in failed logins per email prefix. middleBrick’s Continuous Monitoring (Pro plan) can schedule scans to detect regressions in rate limiting or authentication behavior over time, and the GitHub Action can fail builds if a security score drops below your chosen threshold.