Brute Force Attack in Nestjs with Api Keys
Brute Force Attack in Nestjs with Api Keys — how this specific combination creates or exposes the vulnerability
A brute force attack against an API key mechanism in a NestJS application typically involves an attacker systematically trying many possible key values to discover a valid key. Because API keys are often bearer tokens sent in headers, if the endpoint does not enforce strict rate limiting or account lockout, an attacker can make many rapid requests with different key guesses. NestJS does not inherently prevent this; it relies on the application to implement protections at the route or guard level.
When API keys are stored or compared inefficiently (for example, using plain string comparisons without constant-time checks) or when key validation logic is spread across middleware and guards in a way that bypasses throttling, the attack surface grows. An attacker may probe endpoints that accept keys in query parameters, headers, or cookies, especially if some routes are unauthenticated or weakly guarded. In a black-box scan, such patterns can be detected as BFLA/Privilege Escalation or Authentication weaknesses, where weak key validation or missing rate limiting allows enumeration or unauthorized access.
Moreover, if the NestJS app exposes OpenAPI documentation, an API key scheme defined in the spec may not be enforced at runtime, creating a mismatch between documented and actual behavior. Scanning tools can correlate missing rate limiting on key-accepting routes with the presence of API key authentication to flag the risk. Because API keys are often long-lived credentials, successful brute force attempts can lead to prolonged unauthorized access, data exposure, or abuse of downstream services.
Api Keys-Specific Remediation in Nestjs — concrete code fixes
To mitigate brute force risks around API keys in NestJS, combine strict rate limiting, constant-time comparison, and centralized validation. Below are concrete, working examples.
1. Rate limiting with a custom decorator
Use a decorator that checks a per-key request count in a cache (e.g., Redis or an in-memory store) and rejects excessive attempts. This example uses a simple in-memory map for clarity; in production, use a distributed store.
@Injectable()
export class RateLimitGuard implements CanActivate {
private requests = new Map<string, number[]>();
private readonly windowMs = 60_000; // 1 minute
private readonly maxRequests = 30;
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const key = request.headers['x-api-key'] || request.query['api_key'];
if (!key) return false;
const now = Date.now();
const timestamps = this.requests.get(key) || [];
const recent = timestamps.filter(t => now - t < this.windowMs);
if (recent.length >= this.maxRequests) {
throw new HttpException('Too many requests', HttpStatus.TOO_MANY_REQUESTS);
}
recent.push(now);
this.requests.set(key, recent);
return true;
}
}
2. Constant-time key comparison
Avoid early exit comparisons that leak timing information. Use a constant-time check to compare the incoming key with stored keys.
import { timingSafeEqual } from 'crypto';
export function validateApiKey(input: string, storedKey: string): boolean {
const inputBuf = Buffer.from(input);
const storedBuf = Buffer.from(storedKey);
if (inputBuf.length !== storedBuf.length) {
// Use a dummy comparison to keep timing consistent
const dummy = Buffer.alloc(storedBuf.length);
timingSafeEqual(inputBuf.fill(0), dummy);
return false;
}
return timingSafeEqual(inputBuf, storedBuf);
}
3. Centralized key validation in a guard
Apply the rate limit guard and constant-time validation together in an auth guard. This ensures every route using the guard inherits the protections.
@Injectable()
export class ApiKeyAuthGuard implements CanActivate {
constructor(private readonly rateLimiter: RateLimitGuard) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (!this.rateLimiter.canActivate(context)) {
return false;
}
const request = context.switchToHttp().getRequest<Request>();
const provided = request.headers['x-api-key'] || request.query['api_key'];
const expected = process.env.API_KEY_STORE?.[request.ip] || '';
return validateApiKey(provided, expected);
}
}
4. Apply guards globally or per route
In your main application file, use the guard to protect routes that accept API keys. This example shows route-specific usage; you can also apply it globally via APP_GUARD.
@Controller('api')
@UseGuards(ApiKeyAuthGuard)
export class SecureController {
@Get('data')
getData() {
return { message: 'Access granted' };
}
}
5. Complement with infrastructure protections
While code-level mitigations are essential, also enforce rate limits at the edge or gateway in front of your NestJS service. This reduces the load on the application and provides an additional layer of defense against high-volume brute force attempts.