HIGH cache poisoningspring bootbearer tokens

Cache Poisoning in Spring Boot with Bearer Tokens

Cache Poisoning in Spring Boot with Bearer Tokens — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when untrusted or malicious data is written into a cache and subsequently served to other users. In a Spring Boot application that uses HTTP caching (for example via Cache-Control headers or a caching filter/interceptor) and relies on Bearer tokens for authorization, a common mistake is keying cache entries primarily by request path or a subset of headers/parameters while neglecting the Authorization header or the user identity encoded in the token.

Consider an endpoint /api/account/profile that returns sensitive profile information. If the response is cached with a key derived only from the request URI and query parameters, and the Authorization header (containing a Bearer token) is excluded from the cache key, two different users can inadvertently share cached responses. User A (with a valid Bearer token) requests the resource and a cached response is stored. When User B (with their own Bearer token) subsequently requests the same URI, the cache may serve User A’s response, exposing User A’s data to User B. This violates the principle that authenticated responses must be scoped to the authenticated principal.

Spring Boot applications often use HttpCacheHeaders or frameworks like Spring Cache with a custom key generator. If the key generator does not incorporate the token’s claims — such as the subject (sub) or client roles — the cache does not differentiate between users. Additionally, if the application caches responses at a gateway or reverse proxy layer and forwards Bearer tokens in headers without excluding them from cache normalization, the same issue arises. Attackers can probe endpoints that reflect user-specific data and attempt to retrieve another user’s information by leveraging predictable or shared cache entries.

Another variant involves query parameters that influence authorization but are omitted from cache keys. For instance, an endpoint /api/reports might accept a role=admin parameter to filter data. If the cache key ignores this parameter and the Authorization Bearer token, a lower-privileged user could receive cached admin-level data. This is especially risky if token introspection or validation is performed once and the result is cached without considering parameter or scope differences.

Real-world attack patterns include probing endpoints with swapped or replayed Bearer tokens to observe whether cached responses differ. If the application fails to bind cache entries to the token’s identity or scope, an attacker might escalate access by iterating through known user identifiers or roles. While this does not directly modify server-side state, it enables horizontal or vertical privilege escalation through cached data disclosure.

From an OWASP API Top 10 perspective, this behavior aligns with Broken Object Level Authorization (BOLA) and data exposure risks. Proper remediation requires ensuring that any data cached for one authenticated context is not served to another. This is not only a performance consideration but a core security requirement when tokens are used for access delegation.

Bearer Tokens-Specific Remediation in Spring Boot — concrete code fixes

To prevent cache poisoning when using Bearer tokens, ensure the cache key explicitly includes token-derived identity and scope. In Spring Boot, you can customize the cache key generation so that the subject (sub) or roles from the JWT are part of the key. Below is an example using Spring Cache with a custom key generator that incorporates the authenticated user’s name and roles extracted from the Bearer token.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public KeyGenerator userAwareKeyGenerator() {
        return (target, method, params) -> {
            StringBuilder key = new StringBuilder();
            key.append(target.getClass().getName()).append(".").append(method.getName()).append(":");
            // Assume SecurityContextHolder is available and contains an Authentication with JWT details
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && authentication.isAuthenticated() && !(authentication instanceof AnonymousAuthenticationToken)) {
                // Extract name and roles from token claims; adjust to your JWT structure
                key.append("user=").append(authentication.getName()).append(";roles=");
                authentication.getAuthorities().forEach(grantedAuthority -> key.append(grantedAuthority.getAuthority()).append(","));
            }
            // Include method parameters that affect authorization, e.g., role query param
            for (Object param : params) {
                if (param instanceof String) {
                    key.append("param=").append(param);
                }
            }
            return key.toString();
        };
    }
}

When declaring a cacheable method, reference this key generator and ensure the Authorization header is not used as a raw string but transformed into claims used in the key.

@Cacheable(value = "userProfiles", keyGenerator = "userAwareKeyGenerator")
public UserProfile getProfile(String userId) {
    // Service logic that returns user-specific data
    return userService.findProfileById(userId);
}

If you are using a WebFilter to validate Bearer tokens, avoid caching responses at the filter or gateway level without incorporating the token’s principal into the cache key. For example, do not normalize cache entries based solely on request.getRequestURI(). Instead, build a composite key that includes the user identity from the validated token.

Additionally, configure cache-control headers to differentiate between private and shared caches. Responses containing sensitive user data should include Cache-Control: no-store or Cache-Control: private to prevent shared caches from storing them. For endpoints that must be cached, scope the caching to the authenticated user by including the user ID or tenant ID in the cache key, as shown above.

Below is an example of a WebSecurity configuration that ensures Bearer tokens are validated and available for use in cache key generation:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        return http.build();
    }
}

With this setup, the JWT’s claims are accessible via the Authentication object, enabling a cache key that correctly isolates data per token. This approach mitigates cache poisoning by ensuring cached responses are bound to the specific identity and permissions encoded in the Bearer token.

Frequently Asked Questions

How can I verify that my Spring Boot cache is not mixing responses between users with different Bearer tokens?
Test by authenticating as two different users with distinct tokens against the same cached endpoint and confirm that each user only receives their own data. Inspect cache storage (e.g., ConcurrentMap, Redis) to ensure keys include user identity or token claims and do not collide across users.
Should I ever cache responses that include sensitive data even if I use Bearer tokens?
Generally avoid caching sensitive data unless necessary, and if caching is required, ensure the cache key incorporates the token’s principal and scope and that cache-control headers restrict storage to private caches. Use no-store for highly sensitive endpoints.