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.