Brute Force Attack in Sails
How Brute Force Attack Manifests in Sails
Brute force attacks in Sails.js applications typically target authentication endpoints where attackers systematically try username/password combinations to gain unauthorized access. The Sails framework's flexible routing and Waterline ORM make it particularly vulnerable when developers don't implement proper rate limiting or authentication safeguards.
The most common attack vector is the login endpoint. In a typical Sails application, you might have a controller like this:
module.exports = {
login: async function (req, res) {
const { email, password } = req.allParams();
const user = await User.findOne({ email: email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const match = await sails.bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({ error: 'Invalid credentials' });
}
return res.json({ token: jwt.sign({ id: user.id }, process.env.JWT_SECRET) });
}
}Without rate limiting, an attacker can hammer this endpoint with thousands of requests per minute. The Waterline ORM's findOne() method executes a database query for each attempt, creating a significant load on your database. Attackers often use credential stuffing attacks with leaked username/password combinations from other breaches.
Another Sails-specific vulnerability arises from the framework's blueprint API. If you have a User model and haven't disabled blueprints, Sails automatically generates RESTful routes including /user/login. Without proper authentication policies, these endpoints become attack surfaces:
// config/blueprints.js
module.exports.blueprints = {
actions: true,
rest: true,
shortcuts: true
}This configuration exposes CRUD operations that attackers can exploit. For example, they might use the find() blueprint to enumerate valid usernames before attempting password attacks. The Sails.js blueprint system, while convenient for development, becomes a liability when deployed without security policies.
Session-based attacks also affect Sails applications. If you're using Sails's built-in session management with default configurations, attackers can exploit predictable session IDs or brute force session tokens. The framework's flexible authentication middleware allows various strategies, but improper configuration can leave endpoints exposed.
Sails-Specific Detection
Detecting brute force attacks in Sails applications requires monitoring both application logs and network traffic patterns. The first sign is usually a spike in failed authentication attempts. You can add logging to your authentication controller:
const loginAttempts = {};
module.exports = {
login: async function (req, res) {
const { email } = req.allParams();
if (!loginAttempts[email]) {
loginAttempts[email] = { count: 0, firstAttempt: Date.now() };
}
loginAttempts[email].count++;
if (loginAttempts[email].count > 5) {
const timeSinceFirst = Date.now() - loginAttempts[email].firstAttempt;
if (timeSinceFirst < 300000) { // 5 minutes
sails.log.warn(`Brute force attempt detected for ${email}`);
return res.status(429).json({ error: 'Too many attempts' });
}
}
// reset after 5 minutes of inactivity
if (Date.now() - loginAttempts[email].firstAttempt > 300000) {
loginAttempts[email] = { count: 1, firstAttempt: Date.now() };
}
// ... rest of authentication logic
}
}For production environments, you need more sophisticated monitoring. The Sails.js framework integrates well with Winston for logging, and you can set up alerts for suspicious patterns. Look for:
- Rapid sequential failed logins from the same IP
- Multiple failed attempts across different accounts in a short timeframe
- Authentication attempts at unusual hours for your user base
- Geographic anomalies (logins from distant locations in impossible timeframes)
middleBrick's automated scanning can detect brute force vulnerabilities without any configuration. When you run middlebrick scan https://yourapp.com from the CLI, it tests your authentication endpoints with rate limiting bypasses and credential stuffing patterns. The scanner identifies endpoints that lack rate limiting, return detailed error messages that help attackers, and allow unlimited authentication attempts.
The middleBrick dashboard shows you exactly which endpoints are vulnerable to brute force attacks. It tests each endpoint with 100+ variations, checking for proper rate limiting implementation, response consistency, and lockout mechanisms. For Sails applications specifically, it identifies blueprint endpoints that might be exposed and tests them against common attack patterns.
Sails-Specific Remediation
Remediating brute force vulnerabilities in Sails requires a multi-layered approach. Start with rate limiting using Sails's built-in middleware or third-party packages. The most effective solution is the sails-hook-bunyan with rate limiting middleware:
// api/policies/rateLimit.js
const rateLimit = require('express-rate-limit');
module.exports = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: {
error: 'Too many authentication attempts from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});Apply this policy to your authentication routes in config/policies.js:
module.exports.policies = {
UserController: {
login: ['rateLimit', 'isAuthenticated'],
logout: true,
// other actions
},
// other controllers
};For database-level protection, use Waterline's query optimization and add indexes to frequently queried fields:
// api/models/User.js
module.exports = {
attributes: {
email: {
type: 'string',
unique: true,
required: true,
index: true // critical for authentication performance
},
password: {
type: 'string',
required: true
}
},
beforeCreate: function (values, cb) {
// hash password before saving
if (values.password) {
const bcrypt = require('bcrypt');
const saltRounds = 10;
values.password = bcrypt.hashSync(values.password, saltRounds);
}
cb();
}
};Implement account lockout mechanisms to prevent credential stuffing:
const loginLockouts = {};
module.exports = {
login: async function (req, res) {
const { email } = req.allParams();
// Check if account is locked
if (loginLockouts[email] && loginLockouts[email].lockedUntil > Date.now()) {
return res.status(423).json({
error: 'Account temporarily locked due to multiple failed attempts'
});
}
// ... authentication logic
if (/* authentication failed */) {
// Increment failure count
if (!loginLockouts[email]) {
loginLockouts[email] = { failures: 0, lockedUntil: 0 };
}
loginLockouts[email].failures++;
if (loginLockouts[email].failures >= 5) {
loginLockouts[email].lockedUntil = Date.now() + 15 * 60 * 1000; // 15 min lockout
}
return res.status(401).json({ error: 'Invalid credentials' });
}
// On successful login, reset failures
if (loginLockouts[email]) {
loginLockouts[email].failures = 0;
loginLockouts[email].lockedUntil = 0;
}
}
}Disable blueprint endpoints that aren't needed in production by setting config/blueprints.js:
module.exports.blueprints = {
actions: false,
rest: false,
shortcuts: false,
prefix: ''
};For API endpoints, use Sails's CORS configuration to restrict access to trusted origins:
// config/cors.js
module.exports.cors = {
allRoutes: false,
origin: process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : ['http://localhost:1337'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
headers: ['content-type', 'authorization']
};Finally, implement proper error handling that doesn't leak information. Always return the same response for both invalid usernames and incorrect passwords:
const genericErrorResponse = { error: 'Invalid credentials' };
module.exports = {
login: async function (req, res) {
const { email, password } = req.allParams();
try {
const user = await User.findOne({ email: email });
if (!user) {
// Always perform hash comparison even if user doesn't exist
// to prevent timing attacks
await sails.bcrypt.compare(password, '$2b$10$invalidhash');
return res.status(401).json(genericErrorResponse);
}
const match = await sails.bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json(genericErrorResponse);
}
return res.json({ token: jwt.sign({ id: user.id }, process.env.JWT_SECRET) });
} catch (err) {
sails.log.error('Authentication error:', err);
return res.status(500).json({ error: 'Authentication service unavailable' });
}
}
}