HIGH password sprayingactixfirestore

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": "user@example.com", "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.

Frequently Asked Questions

Why does returning the same HTTP status code for missing accounts and bad passwords help mitigate password spraying?
Uniform responses prevent attackers from discovering valid usernames via timing or status-code differences. By keeping behavior constant-time and returning a generic error, you remove the signal an attacker needs to prioritize accounts during a spray.
Can Firestore security rules alone stop password spraying in an Actix app?
Firestore security rules are designed for document-level access control, not authentication-rate policies. They cannot enforce per-IP or per-identifier rate limits for login flows. You must implement rate limiting in your Actix application layer or via an API gateway to effectively mitigate spraying.