HIGH cache poisoningnestjs

Cache Poisoning in Nestjs

How Cache Poisoning Manifests in Nestjs

Cache poisoning in Nestjs applications typically occurs when untrusted user input influences cache keys or when cached responses are served to unauthorized users. The framework's built-in caching mechanisms, while powerful, can introduce subtle vulnerabilities if not implemented with security in mind.

One common pattern involves using request parameters directly as cache keys. Consider this vulnerable controller method:

@Controller('users')
export class UsersController {
  @Get(':id')
  @Cache('5m')
  async getUser(@Param('id') userId: string) {
    return this.userService.findById(userId);
  }
}

This code appears secure but has a critical flaw: if an attacker requests /users/1 and then /users/1%0A (where %0A is a newline character), they might cause the cache to store a response keyed by 1 . A subsequent request for /users/1 might then retrieve the poisoned cache entry, potentially exposing data from a different user.

Another manifestation occurs with query parameter manipulation. When using Nestjs's @Cache decorator with query parameters:

@Get('search')
@Cache('10m')
async search(@Query() params: Record) {
  return this.productService.search(params);
}

An attacker could craft specific query parameter combinations that cause the cache to store responses for parameter permutations that shouldn't be cached together, leading to data leakage between different search contexts.

Cross-site request forgery (CSRF) can also enable cache poisoning in Nestjs applications. If your application doesn't properly validate the origin of requests and uses caching for authenticated endpoints, an attacker could trick a victim into making a request that populates the cache with malicious data, which is then served to other legitimate users.

Middleware-based caching introduces additional risks. A custom cache middleware might look like this:

export class CacheMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const cacheKey = req.url + JSON.stringify(req.query);
    const cached = await this.cache.get(cacheKey);
    if (cached) {
      return res.send(cached);
    }
    res.send = (body: any) => {
      this.cache.set(cacheKey, body);
      return res.json(body);
    };
    next();
  }
}

This implementation is vulnerable to cache poisoning because it doesn't validate or sanitize the cache key components, allowing attackers to manipulate the cache storage structure.

Nestjs-Specific Detection

Detecting cache poisoning vulnerabilities in Nestjs applications requires a combination of static analysis and runtime testing. The most effective approach involves examining how cache keys are constructed and what data is being cached.

Start by auditing your controllers for caching patterns. Look for these specific indicators:

grep -r "@Cache" src/ --include="*.ts"
grep -r "cache" src/ --include="*.ts" | grep -E "(set|get|del)"

Focus on finding instances where user input directly influences cache keys without validation. Pay special attention to:

  • Route parameters used in cache keys
  • Query parameters included in cache operations
  • Request headers used for cache identification
  • Authentication tokens or session IDs in cache keys

middleBrick's API security scanner can detect cache poisoning vulnerabilities by analyzing your Nestjs application's runtime behavior. The scanner tests for:

  • Cache key manipulation attempts using special characters
  • Parameter pollution attacks on cached endpoints
  • Cross-user data exposure through cached responses
  • Time-based cache poisoning where responses change over time

Here's how middleBrick identifies cache poisoning in Nestjs applications:

middlebrick scan https://your-nestjs-app.com/api/users/1 \
  --test-cache-poisoning \
  --cache-variants "1,1%0A,1%20,1%2F"

The scanner attempts to poison the cache by requesting slightly modified versions of the same resource, then verifies if the poisoned data is served to other users. It also tests for parameter pollution by sending multiple values for the same parameter:

GET /api/users?id=1&id=2
GET /api/users?id=1&id=malicious

middleBrick's LLM security features are particularly relevant for Nestjs applications using AI/ML endpoints. The scanner checks for system prompt leakage and prompt injection vulnerabilities that could lead to cache poisoning of AI responses.

For comprehensive detection, integrate middleBrick into your CI/CD pipeline:

name: API Security Scan
on: [push, pull_request]
jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: npm install -g middlebrick
      - run: middlebrick scan https://staging.your-app.com \
          --fail-below B \
          --report-json

Nestjs-Specific Remediation

Remediating cache poisoning vulnerabilities in Nestjs requires a defense-in-depth approach. Start by implementing proper cache key normalization and validation.

For route parameter caching, use a cache key factory that normalizes and validates inputs:

export function createSafeCacheKey(...parts: string[]): string {
  return parts
    .map(part => {
      if (!part) return 'empty';
      return part.trim().replace(/[^a-zA-Z0-9-]/g, '_');
    })
    .join(':');
}

@Get(':id')
@CacheTTL(300)
async getUser(@Param('id') userId: string) {
  const cacheKey = createSafeCacheKey('user', userId);
  return this.cacheManager.getOrSet(cacheKey, () => 
    this.userService.findById(userId)
  );
}

For query parameter caching, implement parameter whitelisting and normalization:

const ALLOWED_QUERY_PARAMS = ['search', 'page', 'limit'] as const;

type AllowedQueryParams = typeof ALLOWED_QUERY_PARAMS[number];

@Get('search')
@CacheTTL(600)
async search(@Query() params: Record) {
  const filteredParams = Object.keys(params)
    .filter(key => ALLOWED_QUERY_PARAMS.includes(key as AllowedQueryParams))
    .reduce((acc, key) => ({
      ...acc,
      [key]: params[key].trim()
    }), {});

  const cacheKey = createSafeCacheKey('search', JSON.stringify(filteredParams));
  return this.cacheManager.getOrSet(cacheKey, () => 
    this.productService.search(filteredParams)
  );
}

Use Nestjs's built-in cache module with proper configuration to prevent poisoning:

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      ttl: 300,
      max: 1000,
      detectCacheable: true,
      cacheKeyPrefix: 'secure_app:'
    })
  ],
  controllers: [UsersController],
  providers: [UsersService]
})
export class AppModule {}

Implement cache segmentation to prevent cross-user data exposure:

@Injectable()
export class SecureCacheService {
  constructor(private cacheManager: Cache) {}

  async getOrSetForUser(userId: string, key: string, cb: () => Promise) {
    const safeKey = createSafeCacheKey('user', userId, key);
    const cached = await this.cacheManager.get(safeKey);
    if (cached) return cached;
    
    const result = await cb();
    await this.cacheManager.set(safeKey, result, { ttl: 300 });
    return result;
  }
}

@Get(':id')
@UseInterceptors(CacheInterceptor)
async getUser(@Param('id') userId: string, @User() currentUser: User) {
  if (currentUser.id !== userId) {
    throw new UnauthorizedException('Cannot access other users data');
  }
  return this.secureCacheService.getOrSetForUser(
    currentUser.id, 
    `profile:${userId}`,
    () => this.userService.findById(userId)
  );
}

For applications using Redis, configure proper key space notifications and TTL management:

export class CacheMaintenanceService {
  constructor(@Inject('REDIS_CONNECTION') private redis: Redis)

  async cleanupExpiredKeys() {
    const keys = await this.redis.keys('secure_app:*');
    const pipeline = this.redis.pipeline();
    keys.forEach(key => pipeline.pttl(key));
    const results = await pipeline.exec();
    
    results.forEach(([err, ttl], index) => {
      if (ttl === -1 || ttl > 3600000) {
        this.redis.pexpire(keys[index], 300000);
      }
    });
  }
}

Finally, implement comprehensive logging and monitoring for cache operations:

@Injectable()
export class CacheLoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    const start = Date.now();
    
    return next.handle().pipe(
      tap(() => {
        const duration = Date.now() - start;
        this.logger.log(`Cache operation: ${req.url} - ${duration}ms`);
      }),
      catchError(err => {
        this.logger.error(`Cache error: ${req.url}`, err.stack);
        return throwError(() => err);
      })
    );
  }
}

Frequently Asked Questions

How does cache poisoning differ from cache injection in Nestjs?
Cache poisoning involves corrupting existing cached data through manipulation, while cache injection involves storing malicious data in the cache that wasn't there before. In Nestjs, poisoning often occurs through parameter manipulation that causes legitimate responses to be stored under incorrect keys, whereas injection involves crafting requests that store entirely fabricated responses.
Can middleBrick detect cache poisoning in my Nestjs application?
Yes, middleBrick's black-box scanner tests for cache poisoning by attempting to manipulate cache keys through parameter pollution, special character injection, and cross-user data access patterns. The scanner analyzes your API's caching behavior and identifies vulnerabilities where untrusted input influences cache storage or retrieval.