Credential Stuffing in Hapi
How Credential Stuffing Manifests in Hapi
Credential stuffing attacks against Hapi applications typically target authentication endpoints, such as /login or /session, where the framework processes credentials. Hapi's modular architecture means authentication logic is often implemented via plugins like hapi-auth-basic or hapi-auth-cookie. A common vulnerability arises when these endpoints lack rate limiting or account lockout mechanisms, allowing attackers to automate login attempts using credential pairs harvested from previous data breaches.
In a typical Hapi route configuration, an unprotected login handler might look like this:
server.route({
method: 'POST',
path: '/login',
options: {
auth: 'simple', // Uses a basic auth strategy
handler: async (request, h) => {
const { username, password } = request.payload;
// Validate credentials against database
const user = await User.findOne({ username });
if (user && await bcrypt.compare(password, user.passwordHash)) {
request.cookieAuth.set(user);
return h.response({ status: 'authenticated' });
}
throw Boom.unauthorized('Invalid credentials');
}
}
});Here, the auth: 'simple' strategy delegates to a validate function that runs on every request. Without additional constraints, an attacker can script thousands of requests with different password guesses for a single username, or cycle through known username/password pairs across many accounts. Hapi does not impose default limits on authentication attempts, making it the developer's responsibility to enforce throttling or lockout policies at the route or server level.
Another manifestation occurs when Hapi applications use JWT-based authentication without proper revocation checks. If a token is stolen via stuffing, it remains valid until expiration. Hapi's hapi-auth-jwt2 plugin, for example, will accept any validly signed token unless additional context (like a token blacklist) is manually implemented.
Hapi-Specific Detection
Detecting credential stuffing vulnerabilities in a Hapi API requires testing authentication endpoints for both weak validation and missing rate limiting. middleBrick performs black-box scanning by sending sequential login attempts with common passwords (e.g., 123456, password) and observing responses. A lack of rate limiting is indicated by consistent HTTP 200/401 status codes without Retry-After headers or incremental delays. middleBrick's Authentication and Rate Limiting checks flag these issues, mapping them to OWASP API Top 10: API2:2023 — Broken Authentication.
For example, middleBrick might produce this finding in its report:
| Check | Severity | Endpoint | Evidence |
|---|---|---|---|
| Rate Limiting | High | POST /login | No X-RateLimit-* headers; 50 requests succeeded within 10 seconds |
| Authentication | Critical | POST /login | Accepted weak password 123456 for user admin |
To scan a Hapi API yourself, use the middleBrick CLI:
middlebrick scan https://your-hapi-api.com/loginThe output includes a per-category breakdown and prioritized remediation guidance. The scan takes 5–15 seconds and requires no credentials, as it only tests the unauthenticated attack surface.
Hapi-Specific Remediation
Remediating credential stuffing in Hapi involves implementing rate limiting, enforcing strong passwords, and adding account lockout mechanisms. Hapi does not include built-in rate limiting, but the widely used hapi-rate-limit plugin integrates seamlessly. Configure it at the server level to throttle all routes, or apply selectively to authentication endpoints.
1. Install and register hapi-rate-limit:
const HapiRateLimit = require('hapi-rate-limit');
await server.register({
plugin: HapiRateLimit,
options: {
rateLimit: {
max: 5, // 5 requests per minute
timeWindow: 'minute',
headers: true // Adds X-RateLimit-* headers
},
// Apply only to /login to avoid caching issues
routes: {
path: '/login',
method: 'POST'
}
}
});2. Implement account lockout in your auth strategy: Track failed attempts per username (e.g., in Redis) and reject logins after a threshold. Modify your validate function:
const failedAttempts = new Map(); // Replace with persistent store
const validate = async (request, username, password, h) => {
const attempts = failedAttempts.get(username) || 0;
if (attempts >= 5) {
return { credentials: null, artifacts: { error: 'Account locked' } };
}
const user = await User.findOne({ username });
if (user && await bcrypt.compare(password, user.passwordHash)) {
failedAttempts.set(username, 0); // Reset on success
return { credentials: user };
}
failedAttempts.set(username, attempts + 1);
return { credentials: null, artifacts: { error: 'Invalid credentials' } };
};
server.auth.strategy('simple', 'basic', { validate });3. Enforce password complexity during registration: Use joi validation to reject weak passwords.
const schema = Joi.object({
username: Joi.string().required(),
password: Joi.string()
.pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{12,}$'))
.required()
.messages({
'string.pattern.base': 'Password must be 12+ chars with uppercase, lowercase, and number'
})
});After applying these fixes, re-scan with middleBrick to verify the Rate Limiting and Authentication checks pass. The Pro plan's continuous monitoring can alert you if rate limits are accidentally removed in future deployments.