Password Spraying in Adonisjs
How Password Spraying Manifests in Adonisjs
Password spraying in Adonisjs applications often exploits the framework's authentication middleware and session management. Attackers target the login endpoint, typically found in start/routes.ts or controller files, where they systematically try common passwords against many usernames rather than brute-forcing a single account.
The vulnerability commonly appears in Adonisjs's auth middleware configuration. When developers use the default auth middleware without rate limiting, attackers can make unlimited authentication attempts. The login controller might look like:
export default class AuthController {
async login({ auth, request, response }) {
const { email, password } = request.body();
await auth.attempt(email, password);
return response.ok('Authenticated successfully');
}
}Without rate limiting, an attacker can send thousands of requests with different emails but the same common password. Adonisjs's Lucid ORM integration means these authentication attempts hit the database directly, creating a DoS risk through resource exhaustion.
Session fixation attacks also manifest in Adonisjs when session configuration in config/session.ts uses predictable session IDs or lacks proper regeneration. An attacker might fix a session ID before authentication, then maintain access after the victim logs in.
Another Adonisjs-specific manifestation occurs with API token authentication. When using auth: ['api'] middleware on routes, attackers can enumerate valid usernames by observing response times or error messages, then spray passwords against confirmed valid accounts.
Adonisjs-Specific Detection
Detecting password spraying in Adonisjs requires examining both application code and runtime behavior. Start by reviewing your start/routes.ts file for authentication endpoints that lack rate limiting middleware.
Route.post('login', 'AuthController.login').middleware('auth:api'); // Vulnerable if no rate limiting
Route.post('login', 'AuthController.login').middleware(['auth:api', 'throttle:60,5']); // ProtectedmiddleBrick's black-box scanning specifically tests Adonisjs authentication endpoints by sending multiple authentication attempts with common passwords across different usernames. The scanner identifies vulnerable endpoints by:
- Detecting successful authentication with common passwords across multiple accounts
- Measuring response time consistency that indicates lack of rate limiting
- Identifying missing authentication middleware on sensitive routes
- Checking for predictable session ID patterns in responses
The scanner also examines OpenAPI specs if provided, looking for authentication schemas that reveal username enumeration possibilities. For Adonisjs applications using JWT tokens, middleBrick tests for token reuse vulnerabilities where attackers might capture valid tokens and use them without proper scope validation.
Log analysis is crucial for Adonisjs detection. Review your application logs for patterns of failed authentication attempts across many usernames with the same password. Adonisjs's built-in logger can be configured to track authentication failures:
import Logger from '@ioc:Adonis/Core/Logger';
export default class AuthController {
async login({ auth, request, response, logger }) {
const { email, password } = request.body();
try {
await auth.attempt(email, password);
return response.ok('Authenticated successfully');
} catch (error) {
logger.warn('Failed login attempt', { email, error: error.message });
return response.badRequest('Invalid credentials');
}
}
}Adonisjs-Specific Remediation
Remediating password spraying in Adonisjs requires a layered approach using the framework's built-in security features. Start with rate limiting using Adonisjs's @adonisjs/rate-limit package:
import RateLimit from '@ioc:Adonis/Addons/RateLimit';
export default class AuthController {
async login({ auth, request, response }) {
const { email, password } = request.body();
// Rate limit by IP address and email
await RateLimit.validate({
requests: 5,
duration: '5 mins',
key: `${request.ip()}_${email}`,
});
try {
const login = await auth.attempt(email, password);
return response.ok(login);
} catch (error) {
// Always return same response time to prevent timing attacks
await new Promise(resolve => setTimeout(resolve, 500));
return response.badRequest('Invalid credentials');
}
}
}Implement account lockout policies using Adonisjs's cache system to track failed attempts:
import Cache from '@ioc:Adonis/Addons/Cache';
export default class AuthController {
async login({ auth, request, response }) {
const { email, password } = request.body();
const cacheKey = `auth_fail_${email}`;
// Check if account is temporarily locked
const lockUntil = await Cache.get(cacheKey);
if (lockUntil) {
const remaining = Math.ceil((lockUntil - Date.now()) / 1000);
return response.badRequest(`Account locked for ${remaining} seconds`);
}
try {
const login = await auth.attempt(email, password);
// Clear failure count on success
await Cache.delete(cacheKey);
return response.ok(login);
} catch (error) {
// Increment failure count
const failures = await Cache.get(cacheKey, 0);
if (failures >= 3) {
// Lock account for 15 minutes
await Cache.set(cacheKey, Date.now() + 15 * 60 * 1000, 15 * 60);
return response.badRequest('Account temporarily locked');
}
await Cache.set(cacheKey, failures + 1, 5 * 60);
return response.badRequest('Invalid credentials');
}
}
}Secure your session configuration in config/session.ts:
export default class SessionConfig {
public session = {
driver: 'cookie',
cookieName: 'adonis_session',
clearWith: ['user-logout'],
sameSite: true,
httpOnly: true,
secure: true, // Use HTTPS in production
maxAge: 24 * 60 * 60 * 1000, // 24 hours
}
public cookie = {
secret: Env.get('APP_KEY'),
options: {
httpOnly: true,
sameSite: true,
secure: true,
},
}
}Add authentication middleware to all sensitive routes and use Adonisjs's built-in validation for login attempts:
import { schema } from '@ioc:Adonis/Core/Validator';
export default class AuthController {
async login({ auth, request, response }) {
const loginSchema = schema.create({
email: schema.string({}, [schema.rules.email()]),
password: schema.string({}, [schema.rules.minLength(8)]),
});
const payload = await request.validate({ schema: loginSchema });
// Rate limiting and authentication logic here...
}
}