Cache Poisoning in Spring Boot with Hmac Signatures
Cache Poisoning in Spring Boot with Hmac Signatures
Cache poisoning occurs when an attacker causes a cache to store malicious or incorrect responses that are later served to other users. In a Spring Boot application that uses Hmac Signatures to protect cached data, the vulnerability typically arises when the cache key is derived from or influenced by attacker-controlled input without adequate integrity verification. If the application caches responses keyed by a combination of client-supplied values (e.g., query parameters, headers, or path variables) and uses Hmac Signatures only to sign the response body or a subset of the cache key, an attacker can manipulate the key space to reuse or inject entries into the cache.
Consider a Spring Boot service that caches weather data keyed by location and includes an Hmac Signature to validate the response. If the cache key is built from the raw location parameter and the signature is computed over the response only, an attacker can supply a location like london;admin and, if the application normalizes or splits the key incorrectly, cause the cache to store or retrieve entries under a different logical key. The Hmac Signature on the response does not protect the key itself, so subsequent requests for london might inadvertently receive the poisoned entry if the cache lookup logic is flawed. This can lead to privilege escalation (e.g., an admin context being inferred), incorrect data being served, or injection of malicious content into downstream consumers that rely on the cached value.
Spring Boot applications using HTTP caching (e.g., with Cache-Control headers) or in-memory caches (e.g., ConcurrentHashMap, Caffeine) often combine Hmac Signatures to ensure data integrity. However, if the signature does not cover the full cache key, the boundary between authenticated data and unauthenticated key material blurs. This misalignment is a common root cause of cache poisoning: the signature validates the payload, but the key used to store and retrieve the payload is not protected. Additionally, if the application deserializes or reuses cache keys from multiple sources (e.g., request parameters, headers, and user IDs) without canonicalization, the attack surface expands. For example, an attacker might send a request with crafted headers that cause the application to store a cache entry under a key that benign users later request, effectively achieving a stored XSS or data manipulation via the cache.
To illustrate, imagine a vulnerable endpoint that builds a cache key as key = location + ":" + userId and signs only the response body. An attacker can set userId to a value that changes the key in unexpected ways (e.g., including characters that affect key normalization). The Hmac Signature does not cover the key, so the cache treats the manipulated key as valid. When a legitimate user requests data for the same location but a different userId, the cache may return the poisoned entry if the key resolution logic is not strict. Real-world cache poisoning in this context does not require breaking the Hmac algorithm; it exploits the integration between caching and signature placement, highlighting the need to bind the signature to the exact key used for cache storage and retrieval.
Hmac Signatures-Specific Remediation in Spring Boot
Remediation centers on ensuring that the Hmac Signature covers all components that determine cache storage and retrieval, and that cache keys are constructed and compared in a canonical, attacker-resistant way. In Spring Boot, this means including the full cache key (including any user-supplied or normalized inputs) within the signed payload, or at minimum ensuring that the key is derived from a signed context. Below are concrete code examples that demonstrate a secure approach.
Example 1: Signing the Full Cache Key
Compute the Hmac over a string that combines the cache key and the response body. This ensures that any change to the key invalidates the signature.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class HmacUtil {
private static final String HMAC_ALGORITHM = "HmacSHA256";
public static String calculateHmac(String data, String secret) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
mac.init(secretKey);
byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hmacBytes);
}
}
Use this utility in a service that caches weather data:
@Service
public class WeatherService {
private final Map cache = new ConcurrentHashMap<>();
private final String hmacSecret = System.getenv("HMAC_SECRET");
public String getWeather(String location, String userId) throws Exception {
String key = canonicalKey(location, userId);
String cacheKey = key;
CacheEntry entry = cache.get(cacheKey);
if (entry != null && isValid(entry.hmac, cacheKey + entry.body)) {
return entry.body;
}
String body = fetchWeatherFromApi(location, userId);
String hmac = HmacUtil.calculateHmac(cacheKey + body, hmacSecret);
cache.put(cacheKey, new CacheEntry(body, hmac));
return body;
}
private String canonicalKey(String location, String userId) {
// Normalize to prevent key manipulation via special characters
return location.trim().toLowerCase() + ":" + userId.trim();
}
private boolean isValid(String expected, String actual) throws Exception {
String computed = HmacUtil.calculateHmac(actual, hmacSecret);
return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
computed.getBytes(StandardCharsets.UTF_8));
}
private static class CacheEntry {
String body;
String hmac;
CacheEntry(String body, String hmac) { this.body = body; this.hmac = hmac; }
}
}
Example 2: Including Key Material in the Signed Payload
When you cannot avoid using raw request-derived values in the cache key, include them explicitly in the Hmac input rather than relying on the signature to protect the response body alone.
@RestController
@RequestMapping("/weather")
public class WeatherController {
private final Map cache = new ConcurrentHashMap<>();
private final String hmacSecret = System.getenv("HMAC_SECRET");
@GetMapping
public String get(@RequestParam String location, @RequestParam String userId) throws Exception {
String key = location + ":" + userId;
String canonicalKey = key.toLowerCase().trim();
String cached = cache.get(canonicalKey);
if (cached != null && isValidHmac(canonicalKey, cached)) {
return cached;
}
String body = fetchWeather(location, userId);
String signed = canonicalKey + ":" + body;
String hmac = HmacUtil.calculateHmac(signed, hmacSecret);
cache.put(canonicalKey, body + ":" + hmac);
return body;
}
private boolean isValidHmac(String data, String stored) throws Exception {
String[] parts = stored.split(":", 2);
if (parts.length != 2) return false;
String expectedHmac = parts[1];
String computed = HmacUtil.calculateHmac(data + ":" + parts[0], hmacSecret);
return MessageDigest.isEqual(expectedHmac.getBytes(StandardCharsets.UTF_8),
computed.getBytes(StandardCharsets.UTF_8));
}
}
These examples ensure that the Hmac covers the exact data used to locate the cached entry. Additional protections include strict key normalization (trimming, lowercasing), avoiding concatenation with ambiguous separators, and using constant-time comparison to prevent timing attacks. MiddleBrick scans can help identify whether cache keys are included in Hmac coverage and flag missing integrity checks as high-severity findings.