Cache Poisoning in Nestjs with Hmac Signatures
Cache Poisoning in Nestjs with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker tricks a caching layer into storing malicious content and serving it to other users. In a NestJS application that uses Hmac Signatures to protect cached responses, misconfiguration or inconsistent verification can weaken the protection and enable poisoning.
Consider an endpoint that returns sensitive profile data and caches the response using a key derived from request parameters and an Hmac. If the application uses only a subset of request properties (e.g., the URL path) to compute the Hmac but later serves cached content for requests that differ in a non-hashed parameter (such as an Accept header or a user-specific query flag), an attacker may cause the same cached entry to be reused in contexts where it should not be valid.
A concrete example: an endpoint /api/users/{id} includes the id in the Hmac input but does not include the X-Forwarded-For or a nonce when caching. A poisoned cache key could be reused across different clients or tenants if the cache key derivation does not account for all dimensions that affect response sensitivity. This can lead to one user seeing another user’s data when the cache is shared across requests that should have been isolated.
Hmac Signatures are designed to ensure integrity and authenticity of a request or cache key, but they do not automatically prevent cache poisoning if the scope of what is signed is too narrow. For instance, signing only the route template while ignoring headers that affect authorization or data classification can allow an attacker-controlled header to change the response without invalidating the signature. Additionally, if the application caches public responses but uses a signing key that is inadvertently exposed or shared across services, an attacker might craft a valid Hmac for a malicious payload and store it in the cache.
In NestJS, this can surface when caching logic is implemented at the interceptor or middleware layer and the Hmac is computed over a partial set of attributes. If the cache key does not incorporate tenant or user context, and the response contains user-specific data, poisoned entries can be served across different users. Similarly, if the application normalizes inputs inconsistently before signing (for example, sorting query parameters differently at cache-lookup versus cache-store time), signature validation may pass while the cached content is incorrect or unsafe.
To mitigate these risks, ensure that the Hmac input includes all request dimensions that affect the response, such as headers that influence authorization, content negotiation, or tenant identification. Avoid caching sensitive responses in shared caches unless the cache key and Hmac scope guarantee isolation per user or tenant. Validate that the same canonicalization rules are applied consistently at both signing and verification stages within the NestJS pipeline.
Hmac Signatures-Specific Remediation in Nestjs — concrete code fixes
Remediation focuses on widening the Hmac scope, canonicalizing inputs, and ensuring cache keys incorporate all attributes that affect response sensitivity. Below are concrete NestJS patterns that reduce cache poisoning risks while preserving the integrity guarantees of Hmac Signatures.
1. Include headers and tenant context in the Hmac input
Ensure the signed string includes headers that affect authorization or data classification. This prevents attackers from changing such headers without invalidating the signature.
import { createHmac } from 'crypto';
export function buildHmac(
method: string,
path: string,
headers: Record,
query: Record,
body: string,
secret: string,
): string {
const canonicalHeaders = Object.keys(headers)
.sort()
.map((k) => `${k.toLowerCase()}:${Array.isArray(headers[k]) ? headers[k].join(',') : headers[k]}`)
.join('\n');
const canonicalQuery = Object.keys(query)
.sort()
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(Array.isArray(query[k]) ? query[k].join(',') : query[k])}`)
.join('&');
const payload = [
method.toUpperCase(),
path,
canonicalHeaders,
canonicalQuery,
body,
].join('\n');
return createHmac('sha256', secret).update(payload).digest('hex');
}
// Usage in a NestJS interceptor
import { ExecutionContext, Injectable, NestInterceptor, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class HmacCacheInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable {
const request = ctx.switchToHttp().getRequest();
const secret = process.env.HMAC_SECRET;
const key = buildHmac(
request.method,
request.path,
request.headers,
request.query,
JSON.stringify(request.body),
secret,
);
// Use key for cache lookup/store, ensuring it covers headers and query
return next.handle().pipe(map((data) => ({ data, key })));
}
}
2. Use a consistent canonicalization strategy across signing and verification
Apply the same sorting, encoding, and header normalization when computing and when validating the Hmac. Avoid differing behavior between cache-lookup and cache-store phases.
// Shared canonicalization utility used in both signing and verification
export function canonicalizeForHmac(
method: string,
originalUrl: string,
headers: Record,
query: Record,
body: any,
) {
const normalize = (value: string) => value.trim().toLowerCase();
const sortedHeaders = Object.entries(headers)
.map(([k, v]) => `${normalize(k)}:${Array.isArray(v) ? v.map(normalize).join(',') : normalize(v)}`)
.sort()
.join('\n');
const url = new URL(originalUrl, 'http://localhost');
const sortedKeys = Object.keys(query).sort();
const queryString = sortedKeys
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(Array.isArray(query[k]) ? query[k].join(',') : query[k])}`)
.join('&');
const payload = [method.toUpperCase(), url.pathname + url.search, sortedHeaders, queryString, JSON.stringify(body)].join('\n');
return payload;
}
// Verification example
export function verifyHmac(
method: string,
originalUrl: string,
headers: Record,
query: Record,
body: any,
receivedHmac: string,
secret: string,
): boolean {
const expected = canonicalizeForHmac(method, originalUrl, headers, query, body);
const actual = createHmac('sha256', secret).update(expected).digest('hex');
// Use timing-safe compare
return createHmac('sha256', secret).update(expected).digest('hex') === receivedHmac;
}
3. Scope cache keys and Hmac to tenant and user context
Include tenant or user identifiers in both the cache key and the Hmac input to prevent cross-user contamination in shared caches.
export function buildHmacWithTenant(
method: string,
path: string,
headers: Record,
query: Record,
body: string,
secret: string,
tenantId: string,
userId: string,
): string {
const payload = [
tenantId,
userId,
method.toUpperCase(),
path,
Object.keys(headers).sort().map(k => `${k}:${headers[k]}`).join('|'),
Object.keys(query).sort().map(k => `${k}=${query[k]}`).join('&'),
body,
].join('|');
return createHmac('sha256', secret).update(payload).digest('hex');
}
With these changes, the Hmac covers the dimensions that affect the response, reducing the likelihood that a poisoned cache entry is accepted as valid. Remember that middleBrick can scan such APIs and surface related findings in categories like Input Validation, Authentication, and BOLA/IDOR, helping you verify that headers and tenant context are properly considered in both runtime behavior and spec definitions.