Api Rate Abuse in Hapi with Api Keys
Api Rate Abuse in Hapi with Api Keys — how this specific combination creates or exposes the vulnerability
Rate abuse in Hapi when using API keys typically arises when rate-limiting is applied at the API key level but is missing or misconfigured on individual endpoints. Without per-key enforcement, a client can make an unlimited number of requests to a vulnerable route, leading to resource exhaustion, denial of service, or brute-force attacks on other controls such as authentication.
Hapi does not enforce rate limits by default. If API keys are used only for identification (for example, read from headers and passed in request artifacts) and not coupled with a rate-limiting strategy, an attacker who obtains a valid key can repeatedly call sensitive endpoints. Scenarios include login, password reset, or token refresh, where each request consumes server-side resources or enables credential guessing.
Another common pattern is validating the API key but applying a global or coarse rate limit. For example, a limit might be enforced for all requests in a minute, but not scoped to a key. This allows a single compromised key to consume the quota, effectively denying service to other legitimate key holders. Additionally, if keys are embedded in client-side code or mobile app binaries, they can be extracted and reused, making abuse easier.
The risk is compounded when combined with findings from other checks such as BOLA/IDOR or missing property-level authorization. An attacker might iterate over valid resource identifiers while staying within a weak rate limit, escalating the impact. Because middleBrick tests unauthenticated attack surfaces, it can detect missing rate-limiting on key-accepted routes, even when authentication is otherwise required for other paths.
Real-world attack patterns mirror OWASP API Top 10 2023:2014 (Rate Limiting) and common brute-force techniques. For example, an attacker may use a stolen key to call /login with different credentials or call /v1/transactions with crafted parameters to infer behavior or exhaust quotas. These are detectable by scanning with tools that exercise endpoints without authentication, highlighting where rate-limiting is absent despite key acceptance.
middleBrick scans identify these gaps by running 12 security checks in parallel, including Rate Limiting and API key handling, and provide a severity-ranked output with remediation guidance. The scan does not modify or block traffic; it reports findings and guidance so teams can adjust their Hapi configuration and key usage policies.
Api Keys-Specific Remediation in Hapi — concrete code fixes
To remediate rate abuse in Hapi when using API keys, enforce rate limits per API key and validate keys server-side before processing requests. Below are concrete, working examples using the hapi-auth-api-key package and the built-in rate-limiting facilities available in the Hapi ecosystem.
1) Basic API key extraction and validation with rate limiting per key
const Hapi = require('@hapi/hapi');
const ApiKeyAuth = require('hapi-auth-api-key');
const RateLimiter = require('hapi-rate-limiter');
const validateKey = async (key, request) => {
// Replace with your key lookup, e.g., database or cache check
const validKeys = new Set(['abc123', 'def456']);
return { isValid: validKeys.has(key), credentials: { key } };
};
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
await server.register([
ApiKeyAuth,
{
plugin: RateLimiter,
options: {
rate: { max: 60, windowMs: 60000 }, // 60 requests per minute
keyExtractor: (request) => {
// Extract API key from header; rate limiter uses this key for bucketing
return request.headers['x-api-key'] || 'unknown';
}
}
}
]);
server.auth.strategy('api-key', 'api-key', { validateKey });
server.auth.default('api-key');
server.route({
method: 'GET',
path: '/public',
options: {
auth: false // allow unauthenticated checks for rate limit visibility
},
handler: (request, h) => ({ message: 'public endpoint' })
});
server.route({
method: 'POST',
path: '/action',
options: {
auth: 'api-key',
plugins: {
rateLimiter: {
rate: { max: 10, windowMs: 60000 } // stricter per-key limit for sensitive actions
}
}
},
handler: (request, h) => ({ message: 'action executed', key: request.auth.credentials.key })
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch((err) => {
console.error(err);
});
This example registers API key authentication and a rate limiter scoped to the x-api-key header. Each key gets its own rate-limit bucket, preventing a single compromised key from exhausting global quotas.
2) Dynamic per-key rate limits with caching (e.g., Redis)
const Hapi = require('@hapi/hapi');
const ApiKeyAuth = require('hapi-auth-api-key');
// Assume redisClient is a connected Redis client
const redisClient = require('./redisClient');
const validateKey = async (key, request) => {
const isValid = await redisClient.isKeyValid(key); // your logic
return { isValid, credentials: { key } };
};
const getRateLimitFor = async (key) => {
const tier = await redisClient.getKeyTier(key); // e.g., 'free', 'pro'
return tier === 'pro' ? { max: 300, windowMs: 60000 } : { max: 60, windowMs: 60000 };
};
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
await server.register(ApiKeyAuth);
server.auth.strategy('api-key', 'api-key', { validateKey });
server.auth.default('api-key');
server.route({
method: 'GET',
path: '/data',
options: {
auth: 'api-key',
handler: async (request, h) => {
const { max, windowMs } = await getRateLimitFor(request.auth.credentials.key);
// Implement token bucket or sliding window via Redis; simplified here
const allowed = await redisClient.checkAndIncrement(request.auth.credentials.key, max, windowMs);
if (!allowed) {
throw Boom.tooManyRequests('Rate limit exceeded for your API key');
}
return { data: 'sensitive' };
}
}
});
await server.start();
console.log('Server running');
};
init().catch((err) => console.error(err));
This pattern uses Redis to store per-key counters and timestamps, enabling dynamic limits based on key tier while avoiding global collisions.
3) Using Hapi’s built-in request rate limiting (v19+)
const Hapi = require('@hapi/hapi');
const ApiKeyAuth = require('hapi-auth-api-key');
const validateKey = async (key, request) => ({
isValid: key === 'secret123',
credentials: { key }
});
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
await server.register(ApiKeyAuth);
server.auth.strategy('api-key', 'api-key', { validateKey });
server.auth.default('api-key');
server.route({
method: 'GET',
path: '/limited',
options: {
auth: 'api-key',
rateLimit: {
max: 100,
timeWindow: 60000,
keyGenerator: (request) => request.auth.credentials.key
}
},
handler: (request, h) => ({ ok: true })
});
await server.start();
console.log('Server running with built-in rate limiting');
};
init().catch((err) => console.error(err));
These examples demonstrate how to bind rate limits to API keys in Hapi. By scoping limits to the key and validating keys server-side, you reduce the risk of abuse while preserving legitimate usage. For ongoing visibility, middleBrick can be used to verify that rate-limiting controls are present and effective across your endpoints.