HIGH cache poisoningnestjstypescript

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();
  }
}

Frequently Asked Questions

Does using TypeScript types prevent cache poisoning in NestJS?
No. TypeScript provides compile-time type safety for variables and function signatures, but it does not prevent logical errors in cache key design. If runtime values such as user ID, roles, or tenant are omitted from cache keys, poisoned caching can occur regardless of strong typing.
How can I verify that my cache keys properly scope sensitive endpoints in a NestJS app?
Review your cache key construction to ensure it includes user identifiers, roles, and tenant context. Test by simulating requests from different users and tenants, and confirm that cached responses are not shared across distinct contexts. Tools that compare runtime responses against expected scope can help validate behavior.