Credential Stuffing in Sails
How Credential Stuffing Manifests in Sails
Credential stuffing in Sails typically leverages automated scripts that submit known username and password pairs to the local authentication endpoint. In Sails, the default action blueprint for logging in (e.g., AuthController.login or a custom POST /auth/login route) becomes the target. Attackers probe for unlocked accounts, using lists of breached credentials to test against the User model. If the application does not enforce per-account rate limiting or robust bot mitigation, Sails may respond with distinct status codes or messages that help attackers enumerate valid users, for example a 200 with a session cookie for a valid credential versus a 401 for an invalid password.
Sails-specific code paths often include the config/policies.js where session authentication policies grant access to controllers, and the Waterline ORM queries in models such as User.findOne({ email: email }) followed by a password comparison using a custom helper or bcrypt.compare. If the login action does not uniformly apply delays or error message normalization, attackers can infer whether an account exists. A typical vulnerable action might look like:
module.exports.login = async function (req, res) {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.unauthorized('Invalid credentials');
}
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.unauthorized('Invalid credentials');
}
return res.ok({ token: await JwtService.issue({ id: user.id }) });
};
This pattern can be abused in a stuffing campaign because the endpoint reliably distinguishes between a nonexistent user and a password mismatch, and it does not impose account-level throttling. Attackers iterate over millions of credentials, relying on Sails’s default behavior to confirm or deny authentication without additional protections.
Sails-Specific Detection
To detect credential stuffing risks in Sails, scanning with middleBrick is effective because it probes the authentication surface without credentials. middleBrick runs checks for Authentication, Rate Limiting, Input Validation, and Unsafe Consumption in parallel, correlating findings across the 12 security checks. For example, if the scan detects that the login endpoint returns different HTTP status codes or response times for valid versus invalid users, it flags this as an authentication weakness. The scan also reviews OpenAPI/Swagger specs (2.0, 3.0, 3.1) with full $ref resolution, cross-referencing definitions with runtime behavior to ensure that documented authentication schemes align with actual enforcement.
You can initiate a scan using the CLI:
middlebrick scan https://api.example.com
In the Web Dashboard, review the Authentication and Rate Limiting sections for indicators such as inconsistent error messaging, missing account lockout, or lack of CAPTCHA challenges after repeated failures. The dashboard provides per-category breakdowns and prioritized findings with severity and remediation guidance, helping you confirm whether Sails authentication logic inadvertently aids credential stuffing.
Sails-Specific Remediation
Remediation in Sails focuses on hardening the login flow and introducing account-level protections while preserving the framework’s conventions. Use built-in policies and hooks to centralize logic, and leverage community libraries for throttling and secure password handling.
- Uniform responses and rate limiting: Ensure login responses do not reveal whether an account exists. Combine this with a rate limiter such as
rate-limiter-flexibleapplied per email or IP. Example policy inconfig/policies.js:
module.exports.policies = {
'AuthController': {
'login': ['rateLimiter', 'validateLoginInput']
}
};
- Input validation: Validate and sanitize inputs in the action or via an API schema library to prevent malformed requests and injection. Example using
sails-hook-validation:
module.exports = {
friendlyName: 'Login',
description: 'Authenticate user',
inputs: {
email: { type: 'email', required: true },
password: { type: 'string', minLength: 8, required: true }
},
exits: {
unauthorized: { responseType: 'unauthorized' }
},
fn: async function (inputs, exits) {
const user = await User.findOne({ email: inputs.email });
if (!user) {
return exits.unauthorized({ message: 'Invalid credentials' });
}
const match = await sails.helpers.bcrypt.compare(inputs.password, user.password);
if (!match) {
return exits.unauthorized({ message: 'Invalid credentials' });
}
return exits.success({ token: await JwtService.issue({ id: user.id }) });
}
};
- Account lockout and monitoring: Implement incremental delays or temporary locks after repeated failures using a store like Redis. Record failed attempts in a model and evaluate thresholds before allowing further attempts.
const Redis = require('ioredis');
const redis = new Redis();
module.exports.login = async function (req, res) {
const { email, password } = req.body;
const key = `login:failures:${email}`;
const failures = await redis.incr(key);
if (failures === 1) {
await redis.expire(key, 300); // 5 minutes window
}
if (failures > 10) {
return res.tooManyRequests('Account temporarily locked');
}
const user = await User.findOne({ email });
if (!user) {
return res.unauthorized('Invalid credentials');
}
const match = await sails.helpers.bcrypt.compare(password, user.password);
if (!match) {
await redis.incr(key);
return res.unauthorized('Invalid credentials');
}
await redis.del(key);
return res.ok({ token: await JwtService.issue({ id: user.id }) });
};
By combining consistent error handling, input validation, and rate limiting, Sails applications can resist credential stuffing while maintaining compatibility with existing authentication patterns.