Credential Stuffing in Sails with Cockroachdb
Credential Stuffing in Sails with Cockroachdb — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated attack where attackers use large lists of breached username and password pairs to gain unauthorized access to user accounts. When a Sails.js application uses CockroachDB as its data store, the interaction between Sails’ session and authentication mechanisms and CockroachDB’s behavior can inadvertently support or expose patterns that facilitate credential stuffing.
Sails does not enforce authentication by default; it provides hooks and policies that developers must configure. If a login endpoint is built without explicit protections—such as rate limiting, account lockout, or multi-factor authentication—an attacker can send many credential attempts against the same or different accounts. CockroachDB, while strongly consistent and horizontally scalable, does not inherently prevent high-frequency authentication requests from a single source. Without application-level controls, an attacker can repeatedly hit the login route, and CockroachDB will process each request as a legitimate transaction, logging each attempt and potentially revealing timing differences or error messages that aid further attacks.
A typical vulnerability pattern in Sails with CockroachDB involves missing or misconfigured rate limiting on the login action, predictable or sequential user identifiers, and verbose error responses. For example, if a Sails controller returns different messages for “user not found” versus “invalid password,” an attacker can enumerate valid usernames. CockroachDB’s SQL interface, often accessed via an ORM like Waterline, may expose subtle timing differences when comparing hashes depending on whether the user record exists, especially if indexes are not uniformly applied. These differences can be detected through statistical analysis of response times across many requests. Additionally, if session identifiers or authentication tokens are stored in client-side cookies without the HttpOnly and Secure flags, or if they are predictable, stolen credentials can be reused across sessions, compounding the risk.
In an automated scan, these issues manifest as findings such as weak rate limiting, verbose error messages, and inconsistent authentication response times. Attackers combine these signals to perform credential stuffing at scale, leveraging breached credential databases to test accounts across many services. The absence of adaptive controls—such as progressive delays, captchas after repeated failures, or anomaly detection—means that Sails applications backed by CockroachDB remain susceptible to account takeover via credential stuffing.
Cockroachdb-Specific Remediation in Sails — concrete code fixes
Remediation focuses on hardening authentication endpoints, standardizing responses, and enforcing rate controls. The following practices and code examples demonstrate how to secure a Sails app using CockroachDB.
- Use consistent response times and messages: Always return the same generic message regardless of whether the username exists, and ensure password hashing uses a strong, slow algorithm like bcrypt.
- Apply rate limiting at the controller or policy level to restrict login attempts per IP or per user identifier.
- Leverage CockroachDB’s strengths—such as strong consistency and unique constraints—to enforce uniqueness and perform reliable lookups with indexed fields.
Example Sails controller with safe authentication logic:
// api/controllers/AuthController.js
const bcrypt = require('bcrypt');
const RATE_LIMIT_WINDOW = 60 * 10; // 10 minutes
const MAX_ATTEMPTS = 5;
module.exports = {
login: async function (req, res) {
const { username, password } = req.allParams();
const ip = req.ip;
// Rate limiting using a CockroachDB-backed model
const attempts = await LoginAttempt.count({ ip }).meta({ useCache: false });
if (attempts >= MAX_ATTEMPTS) {
return res.status(429).send({ error: 'Too many requests' });
}
// Constant-time lookup to reduce timing variance
const user = await User.findOne({ username }).meta({ useCache: false });
if (!user) {
// Still hash to keep timing similar
await bcrypt.hash(password, 10);
return res.status(401).send({ error: 'Invalid credentials' });
}
const match = await bcrypt.compare(password, user.passwordHash);
if (!match) {
await LoginAttempt.create({ ip }).fetch();
return res.status(401).send({ error: 'Invalid credentials' });
}
// Reset attempts on success
await LoginAttempt.destroy({ ip }).meta({ useCache: false });
// Set secure, HttpOnly cookie
res.setCookie('auth_token', user.authToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
return res.ok({ userId: user.id });
},
};
Example CockroachDB schema for users and login attempts:
-- sql/User.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username STRING UNIQUE NOT NULL,
password_hash STRING NOT NULL,
auth_token STRING UNIQUE,
created_at TIMESTAMP DEFAULT now()
);
-- sql/LoginAttempt.sql
CREATE TABLE login_attempts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
ip INET NOT NULL,
created_at TIMESTAMP DEFAULT now()
);
-- Ensure indexes for efficient lookups
CREATE INDEX idx_login_attempts_ip ON login_attempts (ip);
CREATE INDEX idx_users_username ON users (username);
Example policy to enforce authentication for sensitive actions:
// api/policies/authenticated.js
module.exports = async function (req, res, proceed) {
const token = req.cookies.auth_token;
if (!token) {
return res.status(401).send({ error: 'Unauthorized' });
}
const user = await User.findOne({ authToken: token }).meta({ useCache: false });
if (!user) {
return res.status(401).send({ error: 'Unauthorized' });
}
return proceed();
};
These measures reduce the effectiveness of credential stuffing by standardizing responses, enforcing rate limits with durable storage, and ensuring that database interactions are predictable and indexed. They align with OWASP API Security Top 10 controls around authentication and rate limiting.