Cache Poisoning in Spring Boot
How Cache Poisoning Manifests in Spring Boot
Cache poisoning in Spring Boot applications typically occurs when user-controlled input influences cache keys or when cached responses contain sensitive data that gets served to unauthorized users. The most common attack vectors involve Spring's built-in caching abstractions and HTTP-level caching mechanisms.
Spring Boot's @Cacheable annotation is particularly vulnerable when cache keys incorporate request parameters without proper validation. Consider this endpoint:
@RestController
public class UserController {
@GetMapping("/users")
@Cacheable(value = "users", key = "#role")
public List<User> getUsersByRole(@RequestParam String role) {
return userService.findByRole(role);
}
}An attacker can manipulate the 'role' parameter to poison the cache with malicious data or trigger cache stampedes by requesting non-existent roles. The cache key generation using Spring Expression Language (SpEL) evaluates the raw parameter value, creating a direct path from user input to cache storage.
HTTP-level cache poisoning occurs when Spring Boot applications serve cached responses containing sensitive information. The default Spring Boot caching configuration often includes ETag and Last-Modified headers, which can be exploited if the application doesn't properly validate cache freshness:
@GetMapping("/api/data")
public ResponseEntity<Data> getData(@RequestParam String id) {
Data data = dataService.findById(id);
// Vulnerable: no validation of cache staleness
return ResponseEntity
.ok()
.eTag(new ETag(Long.toString(data.getVersion())))
.body(data);
}Another Spring Boot-specific vector involves the @CachePut and @CacheEvict annotations. Improper use can lead to cache desynchronization where stale data persists longer than intended:
@Service
public class OrderService {
@CachePut(value = "orders", key = "#order.id")
public Order updateOrder(Order order) {
// No cache invalidation for related entities
return orderRepository.save(order);
}
}The above code fails to invalidate related caches (like customer orders summary), allowing poisoned data to remain in dependent caches.
Spring Boot-Specific Detection
Detecting cache poisoning in Spring Boot requires examining both application code and runtime behavior. Static analysis should focus on caching annotations and cache key generation logic. Look for patterns where user input directly influences cache keys without sanitization:
// Vulnerable pattern - direct user input in cache key
@Cacheable(value = "search", key = "#query.toLowerCase()")
public List<Result> search(@RequestParam String query) { ... }middleBrick's scanning engine specifically identifies these Spring Boot cache poisoning patterns by analyzing the SpEL expressions in @Cacheable annotations and tracing data flow from HTTP parameters to cache keys. The scanner tests for cache key manipulation by injecting special characters and observing cache behavior.
Runtime detection involves monitoring cache hit rates and response consistency. Spring Boot Actuator provides cache metrics that can reveal poisoning attempts:
@GetMapping("/actuator/caches")
public Map<String, CacheMetrics> getCacheMetrics() {
return cacheManager.getCacheNames().stream()
.collect(Collectors.toMap(
Function.identity(),
name -> new CacheMetrics(
cacheManager.getCache(name).getNativeCache().size(),
cacheManager.getCache(name).getNativeCache().stats().hitCount()
))
);
}middleBrick's black-box scanning tests cache poisoning by making sequential requests with manipulated parameters and checking for inconsistent responses. The scanner also examines HTTP caching headers for proper validation:
// What middleBrick tests for:
// - Cacheable annotations with unsafe SpEL expressions
// - Missing cache key sanitization
// - Inconsistent cache invalidation
// - Unsafe HTTP caching headers
// - Cache desynchronization patternsThe scanner's LLM security module additionally checks for AI-specific cache poisoning where model responses might be cached and served to unauthorized users, a unique capability not found in other scanners.
Spring Boot-Specific Remediation
Spring Boot provides several native mechanisms to prevent cache poisoning. The most effective approach is implementing strict cache key validation and using composite keys that include request context:
@Service
public class SafeUserService {
@Cacheable(value = "users", keyGenerator = "validatedUserKeyGenerator")
public List<User> getUsersByRole(@RequestParam String role) {
// Validate role against allowed values
if (!RoleValidator.isValid(role)) {
throw new InvalidRoleException();
}
return userRepository.findByRole(role);
}
}
@Configuration
public class CacheConfig {
@Bean
public KeyGenerator validatedUserKeyGenerator() {
return (target, method, params) -> {
String role = (String) params[0];
// Create composite key with validation
return "users:" + RoleValidator.sanitize(role) + ":" +
SecurityContextHolder.getContext().getAuthentication().getName();
};
}
}For HTTP-level caching, Spring Boot's CacheControl builder provides fine-grained control over cache headers:
@GetMapping("/api/sensitive-data")
public ResponseEntity<SensitiveData> getSensitiveData(@RequestParam String id) {
SensitiveData data = dataService.findById(id);
return ResponseEntity
.ok()
.cacheControl(CacheControl
.noStore()
.mustRevalidate())
.header("Pragma", "no-cache")
.body(data);
}Spring Boot's @CacheEvict annotation should be used with proper eviction policies to prevent stale data:
@Service
public class OrderService {
@CachePut(value = "orders", key = "#order.id")
public Order updateOrder(Order order) {
return orderRepository.save(order);
}
@CacheEvict(value = "orders", allEntries = true, condition = "#order.status == 'CANCELLED'")
public void cancelOrder(Order order) {
order.setStatus(CANCELLED);
orderRepository.save(order);
}
}For distributed caching with Redis or Hazelcast, Spring Boot provides cache isolation configurations:
// application.yml
spring:
cache:
type: redis
redis:
time-to-live: 300000 # 5 minutes
cache-null-values: false
key-prefix: app:
use-key-prefix: trueThe above configuration ensures cache keys are namespaced and automatically expires entries, reducing the window for cache poisoning attacks.