Credential Stuffing in Express with Firestore
Credential Stuffing in Express with Firestore — how this specific combination creates or exposes the vulnerability
Credential stuffing exploits reused passwords across services. In an Express backend backed by Google Cloud Firestore, the risk arises when authentication endpoints rely solely on username/email and password without additional anti-automation controls. Firestore security rules typically allow read or write access to user documents based on authenticated UID, but they do not inherently enforce rate limits or bot mitigation, which leaves the API surface open to automated credential testing.
During a scan, middleBrick tests unauthenticated attack surfaces across 12 parallel checks, including Authentication, Rate Limiting, and Input Validation. For an Express app using Firestore, a missing or weak rate limiter on the login route enables attackers to submit many credential pairs per second. Because Firestore operations are relatively fast and inexpensive at scale, attackers can run large credential lists without triggering noticeable service degradation, especially if requests are distributed across IPs.
Another contributing factor is verbose error responses. If Express returns different messages for "user not found" versus "incorrect password," attackers can enumerate valid accounts while Firestore rules permit those queries. middleBrick’s Authentication check flags such differences, and its Rate Limiting check verifies whether tokens or sliding windows are enforced. Because Firestore client libraries do not provide built-in throttling, developers must implement limits explicitly; otherwise, the API remains vulnerable to sustained credential spraying.
Additionally, weak password policies and lack of multi-factor authentication amplify risk. Firestore stores user records, and if passwords are not hashed with a strong, adaptive algorithm like bcrypt, a breached credential database can accelerate stuffing campaigns. middleBrick’s Input Validation checks for predictable password patterns and flags weak hashing configuration where applicable. In CI/CD, the GitHub Action can be configured to fail builds when risk scores drop below a defined threshold, preventing deployments of vulnerable authentication flows.
Lastly, unauthenticated endpoints or misconfigured CORS can expose login routes to browser-based abuse. Firestore rules that allow public read access to collections containing user data must be scoped tightly. middleBrick tests for BFLA/Privilege Escalation and Property Authorization to ensure that authenticated users cannot over-privilege access. For Express apps, combining robust middleware, strict Firestore security rules, and continuous monitoring via the Pro plan’s dashboard and alerts reduces the likelihood of successful credential stuffing.
Firestore-Specific Remediation in Express — concrete code fixes
Remediation centers on rate limiting, consistent error handling, strong password storage, and precise Firestore security rules. Below are concrete, working examples for an Express service using Firestore.
Rate limiting with express-rate-limit and Firestore
Apply a rate limiter to authentication routes to throttle requests per identifier or IP. This reduces the feasibility of credential stuffing by introducing delays and blocking abusive clients.
const rateLimit = require('express-rate-limit');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
initializeApp();
const db = getFirestore();
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each identifier to 5 requests per window
keyGenerator: (req) => req.body.email || req.ip,
handler: (req, res) => {
res.status(429).json({ error: 'Too many attempts, please try again later.' });
}
});
app.post('/login', authLimiter, async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required.' });
}
const userDoc = await db.collection('users').doc(email).get();
if (!userDoc.exists) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
const userData = userDoc.data();
// Assume bcrypt.compare is used for password verification
const isValid = await bcrypt.compare(password, userData.passwordHash);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials.' });
}
// Issue token/session
res.json({ token: 'example-jwt-token' });
});
Secure Firestore rules for user data
Rules should enforce ownership and limit public exposure. Use authentication UID to scope access and avoid broad public reads.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
// Avoid allowing list operations on the collection
allow list: if false;
}
// Deny public writes to any user-related paths
match /publicData/{document=**} {
allow read: if true;
allow write: if false;
}
}
}
Consistent error handling and password storage
Use the same response shape and delay for user-not-found and incorrect-password cases to prevent enumeration. Store passwords with bcrypt, and consider adding a lightweight account lockout after repeated failures recorded in Firestore.
async function verifyCredentials(email, password) {
const userDoc = await db.collection('users').doc(email).get();
// Always run hash comparison if user exists to prevent timing attacks
if (userDoc.exists) {
const isValid = await bcrypt.compare(password, userDoc.data().passwordHash);
if (isValid) return { user: userDoc.data(), valid: true };
}
// Simulate hash work even when user not found
await bcrypt.hash('dummy', 10);
return { user: null, valid: false };
}
CI/CD integration
With the Pro plan, continuous monitoring can be enabled so that risk scores are evaluated on a schedule. The GitHub Action can fail a build if the score falls below your defined threshold, preventing vulnerable configurations from reaching production. The MCP Server allows scanning directly from IDEs, helping developers catch issues early during local development.