Cache Poisoning in Nestjs (Typescript)
Cache Poisoning in Nestjs with Typescript — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes a cache to store malicious or incorrect data that is then served to other users. In a NestJS application written in Typescript, this risk arises when responses that vary by requestor, permissions, or parameters are cached without sufficient differentiation. If caching is configured at the HTTP layer or via a custom in-memory/data-layer without considering request context, one user may receive another user’s data or an attacker may force storage of manipulated content.
Typically, cache poisoning is possible when the application caches based only on a subset of the request (for example, path + query keys) while ignoring headers or cookies that change authorization or tenant context. With Typescript, the type safety of request and response objects does not inherently prevent logical mistakes; if the developer omits authorization or tenant identifiers from the cache key, the vulnerability exists regardless of language safety features. In NestJS, common patterns include using interceptors, custom CacheInterceptor wrappers, or integrating a third-party store (e.g., Redis) with a typed DTO layer. If these are implemented without validating that cached entries are scoped to the correct user, role, or tenant, an attacker can craft requests that reuse another’s cached representation.
Consider an endpoint that returns a user profile with an explicit Vary header omitted or incorrectly set. An authenticated user A queries /profile?details=public and the response is cached. If user B, who has admin privileges, requests /profile?details=public but the cache key ignores the Authorization header or scope claims, user B may receive user A’s cached profile. This becomes critical when sensitive fields are conditionally included in the DTO based on roles that are encoded in token claims rather than request parameters. The risk is not in Typescript’s type system but in how runtime values (headers, cookies, JWT claims) are incorporated into cache keys and validity checks.
Additionally, HTTP method misuse can contribute. GET requests are often cached, but if POST or other methods that mutate state are accidentally cacheable without strict validation, an attacker might poison stored representations for subsequent safe reads. In NestJS, developers must ensure that caching logic distinguishes methods, status codes, and authenticated context. Middleware or guards that inject user context into the request should be considered when forming cache keys. Without such scoping, even well-typed DTOs and services can deliver poisoned cached data across sessions.
Typescript-Specific Remediation in Nestjs — concrete code fixes
To mitigate cache poisoning in NestJS with Typescript, focus on precise cache key construction that incorporates request context and strict validation of cached responses. Use runtime values such as user ID, roles, tenant, and relevant headers to differentiate entries. Below are concrete patterns and code examples that align with secure NestJS practices.
1. Scoped cache key generation
Build cache keys from a combination of route, method, and authenticated context. Avoid relying solely on path and query parameters.
import { Injectable } from '@nestjs/common';
import { RedisService } from './redis.service'; // your Redis wrapper
@Injectable()
export class CacheService {
constructor(private readonly redis: RedisService) {}
private buildKey(req: Request): string {
const userId = req.user?.id || 'anonymous';
const roles = req.user?.roles?.join(',') || 'none';
const tenant = req.headers['x-tenant-id'] || 'public';
return `cache:${req.method}:${req.originalUrl}:uid:${userId}:roles:${roles}:tenant:${tenant}`;
}
async get(req: Request): Promise {
const key = this.buildKey(req);
return this.redis.get(key);
}
async set(req: Request, value: T, ttlSec: number): Promise {
const key = this.buildKey(req);
await this.redis.set(key, value, 'EX', ttlSec);
}
}
2. Vary headers and authorization in caching logic
Ensure responses that differ by authorization are not served from the same cache entry. Explicitly include authorization-related headers in the vary rules or exclude them from the cache key if they are already part of user/role scoping.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { CacheService } from './cache.service';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private readonly cacheService: CacheService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const req = context.switchToHttp().getRequest();
const cacheKey = this.cacheService.buildKey(req);
return this.cacheService.get(req).pipe(
map(cached => {
if (cached) {
return cached;
}
return next.handle().pipe(
map(data => {
this.cacheService.set(req, data, 60).catch(() => {});
return data;
})
);
})
);
}
}
3. Avoid caching sensitive or user-specific responses by default
Use explicit opt-in caching for public endpoints and apply stronger controls for private data. For endpoints that must remain user-specific, ensure the cache key includes user identifiers and roles, and set appropriate TTLs.
// public endpoint — safe to cache broadly
@Get('public/health')
@CacheTTL(300)
getHealth() {
return { status: 'ok' };
}
// private endpoint — cache with user scope
@Get('profile')
@UseInterceptors(CacheInterceptor)
getProfile(@Req() req: Request) {
// ensure req.user is populated by auth guard
return this.profileService.getProfile(req.user.id);
}
4. Validate and sanitize inputs before caching
Even with correct keys, ensure that cached values are not manipulated via query or body injection. Validate and normalize parameters before using them to form cache keys or store responses.
import { BadRequestException } from '@nestjs/common';
function normalizeAndValidate(input: string): string {
const trimmed = input.trim();
if (!/^[a-zA-Z0-9\-_]+$/.test(trimmed)) {
throw new BadRequestException('Invalid parameter');
}
return trimmed;
}
5. Middleware to enforce Vary and context scoping
Set Vary headers appropriately and avoid caching responses that contain user-specific data unless the cache key explicitly accounts for that context.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class CacheControlMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
if (req.user) {
res.set('Vary', 'Authorization, x-tenant-id, Accept-Encoding');
} else {
res.set('Vary', 'x-tenant-id, Accept-Encoding');
}
next();
}
}