HIGH password sprayingadonisjsfirestore

Password Spraying in Adonisjs with Firestore

Password Spraying in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability

Password spraying is an authentication attack where an adversary uses a small list of common passwords against many accounts to avoid account lockouts. When AdonisJS applications use Google Cloud Firestore as the user store, specific implementation patterns can unintentionally enable or amplify this risk.

One common pattern is querying Firestore by email without constant-time behavior. For example, an endpoint like /login might call usersRef.where('email', '==', email).limit(1).get(). Because Firestore query performance can vary slightly with existence and indexing, an attacker can infer whether an email is registered. This user enumeration enables targeted spraying against known accounts. In AdonisJS, route handlers often include additional logic such as hashing the provided password and comparing it to the stored hash only if a document exists. This conditional flow leaks account presence through timing differences and response behavior, especially when error handling or logging differs between missing users and invalid credentials.

Firestore security rules can also contribute to exposure. If rules allow read access to user documents based on email claim or a public collection with overly permissive read conditions, an unauthenticated attacker may probe for valid emails by observing rule-permitted query results versus denied errors. Rules that do not uniformly reject unauthorized existence checks effectively leak a directory of registered users. Furthermore, Firestore does not enforce server-side rate limiting; rate control must be implemented in AdonisJS or via Google Cloud Identity/Apigee. Without explicit rate controls, an attacker can execute thousands of login attempts across many accounts within short time windows, increasing the likelihood of successful guesses against weak passwords.

The combination of user enumeration via timing or error differences and missing rate controls means that Firestore-backed AdonisJS APIs often reveal whether specific emails exist and accept password spraying. Attackers commonly start with lists like admin, webmaster, or postmaster and a small set of passwords such as Password1, Company2024, or context-specific terms. If AdonisJS responses do not standardize timing and messages, these campaigns can be refined quickly. The lack of built-in protections requires developers to design resilient authentication flows and to enforce consistent response patterns and throttling.

Firestore-Specific Remediation in Adonisjs — concrete code fixes

To mitigate password spraying in AdonisJS with Firestore, standardize responses, enforce rate limits, and avoid user enumeration. Below are concrete, realistic code examples that you can adapt to your project.

Standardized authentication handler

Ensure login paths perform a constant-time comparison regardless of whether the user exists. Use the Firestore Admin SDK to retrieve the user document, then compare hashes with a time-safe function. Do not branch on document existence in a way that changes timing or messages.

import { AuthenticationException } from '@adonisjs/core/build/standalone';
import { getFirestore, doc, getDoc } from 'firebase-admin/firestore';
import { verify } from 'argon2';

export async function login(email: string, password: string) {
  const db = getFirestore();
  // Always reference the document by known user ID if you have it; otherwise query by email.
  // Querying by email may still be used, but ensure the flow below does not leak timing.
  const usersRef = db.collection('users');
  const q = usersRef.where('email', '==', email).limit(1);
  const snapshot = await q.get();

  // Fallback to a dummy hash comparison when no user is found to prevent timing leaks.
  const dummyHash = '$argon2id$v=19$m=65536,t=3,p=4$Somesalt$DummyHashForConsistency';
  const storedHash = snapshot.empty ? dummyHash : snapshot.docs[0].get('passwordHash');

  try {
    const valid = await verify(storedHash, password);
    if (!valid) {
      throw new AuthenticationException('Invalid credentials', 'E_INVALID_CREDENTIALS');
    }
    // Optionally fetch the user document for a valid login.
    const userDoc = snapshot.empty ? null : snapshot.docs[0];
    return { user: userDoc ? userDoc.data() : null };
  } catch (error) {
    if (error instanceof AuthenticationException) {
      throw error;
    }
    throw new AuthenticationException('Invalid credentials', 'E_INVALID_CREDENTIALS');
  }
}

Rate limiting with a sliding window in AdonisJS

Implement rate limiting at the route level using a memory store or Redis. This prevents excessive attempts across many accounts. Below is an example using AdonisJS middleware with a simple in-memory map; for production, replace with Redis or another shared store.

import { middleware } from '@adonisjs/core';

const attempts = new Map(); // key: identifier (email or IP), value: { count, lastSeen }

export const rateLimit = middleware(async (ctx, next) => {
  const key = ctx.request.ip(); // or use email from body for account-specific throttling
  const now = Date.now();
  const window = 60_000; // 1 minute
  const max = 30; // max attempts per window

  const entry = attempts.get(key);
  if (entry && now - entry.lastSeen < window) {
    entry.count += 1;
  } else {
    entry = { count: 1, lastSeen: now };
    attempts.set(key, entry);
  }

  if (entry.count > max) {
    ctx.response.status(429).send({ error: 'Too many requests' });
    return;
  }

  await next();
});

Firestore security rules to reduce enumeration

Rules should not reveal whether a document exists via read permissions alone. Use allow read only when the caller is authenticated and limit exposure. Combine with application-level checks and avoid public reads on user collections.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      // Allow read only for authenticated users reading their own document.
      allow read: if request.auth != null && request.auth.uid == userId;
      // Restrict writes to authenticated users and enforce data validation.
      allow write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Monitoring and defense in depth

Log failed attempts with standardized responses and integrate with an alerting system. Combine the above with multi-factor authentication and strong password policies to reduce the impact of spraying. The CLI tool middlebrick scan <url> can help identify authentication and enumeration issues during development, while the GitHub Action adds API security checks to your CI/CD pipeline to fail builds if risk scores exceed your threshold.

Frequently Asked Questions

How does standardized response timing prevent password spraying?
Standardized timing removes timing side channels that attackers use to infer whether an email is registered. By performing the hash comparison for a dummy hash when no user is found, the operation takes similar time, preventing attackers from distinguishing valid accounts.
Can Firestore rules alone stop password spraying?
No. Firestore rules manage document access but do not provide application-level rate limiting or timing-safe authentication. You must implement rate limits and consistent authentication flows in AdonisJS to effectively mitigate spraying.