Auth Bypass in Nestjs with Hmac Signatures
Auth Bypass in Nestjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
HMAC signatures are designed to provide request integrity and origin authentication by using a shared secret to sign a canonical representation of the request. In NestJS, developers often implement HMAC verification in guards or interceptors by reading headers (e.g., x-api-key, x-signature, x-timestamp), constructing the signed string, and comparing the computed HMAC with the client-supplied signature. When this flow is implemented incorrectly, it can lead to authentication bypass despite the use of HMAC.
A common vulnerability pattern is signature normalization and canonicalization errors. If the server does not enforce a strict, consistent method to build the string-to-sign, an attacker can manipulate formatting—such as whitespace, newline handling, parameter ordering, or case sensitivity—to produce a different canonical string that still matches the server’s verification logic in an unintended way. For example, including or excluding certain headers (like x-forwarded-for) inconsistently or failing to reject ambiguous query parameters can allow an attacker to forge a valid HMAC without knowing the secret.
Another bypass vector specific to NestJS arises from middleware ordering and execution context. If HMAC verification is implemented in a NestJS guard rather than in earlier middleware, the request may have already been partially processed or transformed by body parsers or global filters. Inconsistent body parsing (e.g., using raw buffers for signature verification while the body parser consumes the stream) can result in different byte representations on the server side, enabling an attacker to send a valid-looking request that fails verification but still proceeds due to permissive error handling. This can lead to a situation where the application treats a failed HMAC check as a fallback to weaker authentication or no authentication at all.
Header smuggling and duplicate headers can also undermine HMAC-based authentication in NestJS. If the server uses a non-canonical source for header values (e.g., taking the first or last of duplicate headers inconsistently), an attacker can supply multiple x-signature or x-timestamp headers to influence the verification outcome. Similarly, case-insensitive header handling that is not applied uniformly may allow an attacker to bypass checks by providing X-Signature or X-SIGNATURE. These inconsistencies enable an attacker to present a request that appears legitimate to the application while not matching the intended signed payload.
Time-sensitive bypasses are another risk when Hmac signatures are used without proper replay protection. If the server only checks that a timestamp is within a broad window and does not enforce strict one-time use or nonce tracking, an attacker can capture a signed request and replay it within the allowed timeframe. In NestJS, without explicit replay prevention integrated into the guard or interceptor, a valid HMAC with a legitimate timestamp can be reused to gain unauthorized access, especially if the endpoint performs sensitive operations based on that authenticated context.
Finally, weak secret management and insecure generation of the signing key can trivially lead to bypass. If the shared secret is hardcoded, logged, or transmitted in the clear, or if key rotation is not practiced, an attacker who discovers the key can generate valid HMAC signatures for any request. In NestJS, this often occurs when environment variables are mishandled or when the same key is used across multiple services without isolation. The result is that the HMAC mechanism, while present, does not meaningfully protect endpoints and becomes a channel for authentication bypass.
Hmac Signatures-Specific Remediation in Nestjs — concrete code fixes
To securely implement HMAC signatures in NestJS, enforce a strict canonicalization process that deterministically builds the string-to-sign. Define a fixed set of headers to include (e.g., x-timestamp, x-api-key) and ensure consistent ordering, case normalization, and trimming. Always use raw request body for verification before any body parser transforms the payload, and reject requests with ambiguous or duplicate signature-related headers.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { createHmac } from 'crypto';
@Injectable()
export class HmacValidationInterceptor implements NestInterceptor {
private readonly sharedSecret: string;
constructor() {
this.sharedSecret = process.env.HMAC_SECRET || '';
}
async intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const signature = request.headers['x-signature'];
const timestamp = request.headers['x-timestamp'];
const apiKey = request.headers['x-api-key'];
if (!signature || !timestamp || !apiKey) {
throw new Error('Missing required HMAC headers');
}
// Enforce a canonical string-to-sign
const payload = request.rawBody || ''; // ensure raw body is available via middleware
const toSign = [
apiKey.trim(),
timestamp.trim(),
payload
].join('\n');
const expected = createHmac('sha256', this.sharedSecret).update(toSign).digest('hex');
// Constant-time comparison to avoid timing attacks
const isValid = this.constantTimeCompare(expected, signature as string);
if (!isValid) {
throw new Error('Invalid HMAC signature');
}
// Optional: reject duplicate headers at the edge before reaching NestJS pipelines
return next.handle();
}
private constantTimeCompare(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;
}
}
Ensure middleware captures the raw body before JSON parsing so the exact bytes used by the client are verifiable on the server. Configure the HMAC verification as an early middleware or within a guard that runs before business logic, and explicitly disallow duplicate or case-variant signature headers to prevent smuggling.
// Example raw body capture middleware in NestJS
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
req.rawBody = data;
next();
});
}
}
Apply this middleware globally or on specific HMAC-protected routes. Enforce strict timestamp validation with a narrow acceptable window and implement replay protection using nonces or a short-lived cache to prevent reuse. Store the shared secret securely using environment variables or a secrets manager, and rotate keys periodically. Combine HMAC verification with transport-layer encryption and additional runtime security checks to reduce the risk of authentication bypass in NestJS applications.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |