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=maliciousmiddleBrick'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-jsonNestjs-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);
})
);
}
}