Cache Poisoning in Spring Boot with Basic Auth
Cache Poisoning in Spring Boot with Basic Auth — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker causes cached responses to be stored and served to other users, leading to unauthorized data exposure or functionality manipulation. In Spring Boot applications that use HTTP Basic Authentication, this risk can emerge when authenticated and unauthenticated or low-privilege responses are cached based on URL alone, without considering authentication context or Vary headers.
When a Spring Boot endpoint is protected by Basic Auth, the presence or absence of an Authorization header changes the meaning of the response for a given URL. If a caching layer (for example, a reverse proxy, CDN, or in-memory cache) caches the first request it sees—often an unauthenticated request—and then serves that cached response to authenticated users, sensitive data may be inadvertently exposed. Conversely, if a cached authenticated response is reused for unauthenticated users, authentication may be bypassed in the cache layer.
Basic Auth sends credentials in an Authorization header encoded as Base64 (not encrypted). If responses differ based on user roles or identities but caching ignores the Authorization header, one user may see another user’s data. For example, an endpoint like /api/account might return different payloads for different users. Without a proper Vary: Authorization header, a shared cache can store one user’s data and serve it to others, effectively leaking information across users.
Spring Boot’s default behavior does not automatically add Vary headers for Authorization when producing responses. Without explicit configuration, caches that key only on the request URI will treat authenticated and unauthenticated requests as cache-equivalent. This misconfiguration is especially dangerous when responses contain PII, financial data, or session-sensitive information. Attackers may probe endpoints that are intended for authenticated users and observe whether cached responses leak data, or they may deliberately induce the application to cache an authenticated response for public consumption.
Additionally, if the application caches error responses differently for authenticated versus unauthenticated users, an attacker might trigger errors as an unauthenticated user and observe cached error details intended for privileged users. This can aid in reconnaissance for further attacks, such as injection or path traversal. Proper cache control directives and Vary headers are essential to prevent the cache from treating these responses as equivalent.
To detect such issues, scans should compare authenticated and unauthenticated requests to the same endpoint and inspect both response content and headers for missing Vary: Authorization and overly permissive cache directives. MiddleBrick’s authentication, BOLA/IDOR, and Data Exposure checks exercise this scenario by testing cached responses across authentication states and flagging inconsistencies that could lead to cache poisoning.
Basic Auth-Specific Remediation in Spring Boot — concrete code fixes
Remediation focuses on ensuring that cached responses are not shared across different authentication contexts. The primary defenses are correct HTTP cache headers and ensuring that the Authorization header is considered in cache keys.
1. Add Vary: Authorization for authenticated responses
Ensure responses that differ by authentication include Vary: Authorization. In a Spring Boot controller or via a WebMvcConfigurer, you can set this header explicitly.
@RestController
@RequestMapping(/api/account)
public class AccountController {
@GetMapping
public ResponseEntity<Account> getAccount(Authentication authentication) {
Account account = loadAccountFor(authentication.getName());
return ResponseEntity.ok()
.vary("Authorization") // Spring 5.3+ supports this; otherwise set manually
.body(account);
}
}
If your Spring version does not expose vary() directly, set the header manually in a response interceptor or controller advice to guarantee presence.
2. Configure no-store or private caching for sensitive endpoints
For highly sensitive endpoints, prevent shared caching entirely by setting Cache-Control: no-store or no-cache. This ensures each response is re-validated and not stored in shared caches.
@RestController
@RequestMapping(/api/account)
public class AccountController {
@GetMapping
public ResponseEntity<Account> getAccount(Authentication authentication) {
Account account = loadAccountFor(authentication.getName());
return ResponseEntity.ok()
.cacheControl(CacheControl.noStore()) // Prevents caching by shared caches
.body(account);
}
}
3. Use Spring Security to enforce authentication and set headers consistently
Leverage Spring Security to ensure Authorization is required where appropriate and that security headers are applied globally.
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers(/api/account/**).authenticated()
.anyRequest().permitAll()
)
.httpBasic(Customizer.withDefaults())
.headers(headers -> headers
.cacheControl().and()
.addHeaderWriter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Ensure Vary: Authorization for authenticated responses
String auth = request.getHeader("Authorization");
if (auth != null && !auth.isBlank()) {
response.addHeader("Vary", "Authorization");
}
filterChain.doFilter(request, response);
}
}));
return http.build();
}
}
4. Proxy and gateway configuration
If a reverse proxy or API gateway is in front of Spring Boot, ensure it respects Vary headers and does not serve cached responses across different Authorization values. Configure the cache to include Authorization in cache keys for authenticated routes, or disable caching for sensitive paths.
5. Use private caching where appropriate
For user-specific data, prefer private caching (e.g., Cache-Control: private) over shared caching so that intermediaries do not store responses. Combine this with short max-age values for authenticated data.
6. Validate and test with authenticated/unauthenticated pairs
Verify that endpoints return correct cache headers and that responses differ when Authorization is present or omitted. Use automated scans to compare authenticated and unauthenticated requests to the same URL and ensure no sensitive data is returned without proper authentication and cache segregation.