Api Rate Abuse in Nestjs with Hmac Signatures
Api Rate Abuse in Nestjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Rate abuse in a NestJS API that uses HMAC signatures can occur when signature verification is applied only to the request body or selected fields while rate limiting is evaluated before or independently of signature validation. If the server increments rate counters or applies quota checks based on an identifier extracted before verifying the HMAC, an attacker can generate many low-cost requests with distinct, valid signatures that each consume quota or trigger side effects, leading to exhaustion of rate-limited resources or denial of service for legitimate users.
HMAC signatures themselves are not inherently vulnerable; they provide integrity and authenticity when implemented correctly. However, the combination of HMAC-based request authentication and rate limiting introduces a potential mismatch in evaluation order and scope. For example, if the rate limiter uses a client-supplied identifier (such as an API key or a claim from the signature payload) that is accepted before the HMAC is verified, an attacker can exhaust the rate limit with valid-looking requests that carry correct signatures but are intended to abuse a specific operation. Additionally, if the server signs only parts of the request (e.g., the JSON body) and rate limiting is applied to the full request path or query parameters, an attacker can vary unsigned parameters (such as timestamps or nonces included in the URL) to bypass effective throttling while still passing signature checks.
In NestJS, this often manifests when custom guards or interceptors validate signatures after rate limiting middleware has already processed the request. Consider an endpoint that allows a signed POST to transfer funds: if the rate limiter counts requests per signing key before the application verifies the HMAC over the payload, an attacker can send many small, validly signed requests that each pass signature checks but collectively exceed intended transaction limits. Furthermore, inconsistent handling of replay prevention (e.g., relying on a timestamp or nonce included in the signed string without server-side tracking) can allow an attacker to reuse a signed payload within the rate limit window, effectively multiplying the abusive impact of a single valid signature.
To mitigate these risks, ensure that rate limiting is applied after successful signature verification and that the same canonical representation used for signing is considered when enforcing quotas. The server should normalize the data subject to the HMAC (including method, path, selected headers, and body fields) before counting requests, and it should tie rate counters to the authenticated identity derived from the verified signature rather than to unverified inputs. This alignment prevents attackers from consuming rate-limited resources with unverifiable or mismatched requests and reduces the risk of account takeover or resource exhaustion through crafted but valid signatures.
Hmac Signatures-Specific Remediation in Nestjs — concrete code fixes
Remediation centers on verifying HMAC before any rate-limiting logic and using a consistent, canonical string to sign and to rate-limit. Below are concrete NestJS patterns that enforce this order and avoid common pitfalls.
Example: HMAC verification interceptor
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { createHmac } from 'crypto';
@Injectable()
export class HmacValidationInterceptor implements NestInterceptor {
private readonly secret: Buffer = Buffer.from(process.env.HMAC_SECRET!, 'utf8');
async intercept(context: ExecutionContext, next: CallHandler): Promise> {
const request = context.switchToHttp().getRequest();
const signature = request.headers['x-api-signature'] as string | undefined;
const timestamp = request.headers['x-request-timestamp'] as string | undefined;
const nonce = request.headers['x-nonce'] as string | undefined;
if (!signature || !timestamp || !nonce) {
throw new Error('Missing authentication headers');
}
// Build canonical string exactly as the sender used
const payload = {
method: request.method,
path: request.path,
timestamp,
nonce,
body: request.body,
};
const canonical = this.canonicalize(payload);
const expected = 'sha256=' + createHmac('sha256', this.secret).update(canonical).digest('hex');
if (!this.timingSafeEqual(expected, signature)) {
throw new Error('Invalid signature');
}
// Optionally attach verified identity for downstream use
request['auth'] = { subject: payload.path, timestamp: Number(timestamp), nonce };
return next.handle();
}
private canonicalize(p: any): string {
// Keep order deterministic and include only fields the protocol defines
return `${p.method}
${p.path}
${p.timestamp}
${p.nonce}
${typeof p.body === 'string' ? p.body : JSON.stringify(p.body)}`;
}
private timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}
Example: Applying rate limits after verification
import { Injectable } from '@nestjs/common';
import { RateLimitService } from './rate-limit.service';
@Injectable()
export class VerifiedRateLimiter {
constructor(private readonly rateLimitService: RateLimitService) {}
async canProceed(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
// auth is attached by HmacValidationInterceptor
const identity = request['auth'];
if (!identity) {
throw new Error('Unverified request');
}
// Use the verified identity, not raw headers
const allowed = await this.rateLimitService.check({
subject: identity.subject,
windowMs: 60_000,
max: 100,
});
if (!allowed) {
throw new Error('Rate limit exceeded');
}
}
}
Example: Canonicalization shared between sender and server
// Shared helper to ensure both sides produce the same string
export function buildCanonicalString(method: string, path: string, timestamp: string, nonce: string, body: unknown): string {
const normalizedBody = typeof body === 'string' ? body : JSON.stringify(body);
return [method.toUpperCase(), path, timestamp, nonce, normalizedBody].join('\n');
}
Key points:
- Verify HMAC before counting the request against any quota.
- Use a canonical string that includes method, path, timestamp, nonce, and the request body to prevent attackers from altering unsigned parts to bypass limits.
- Bind rate counters to the authenticated identity derived from the verified signature, not to raw query parameters or unverified headers.
- Include replay protection by tracking recently used nonces or timestamps within the rate window when appropriate.