Cache Poisoning in Axum (Rust)
Cache Poisoning in Axum with Rust — how this specific combination creates or exposes the vulnerability
Cache poisoning in Axum applications typically arises when responses keyed by request attributes—such as headers, cookies, or query parameters—are stored in a shared cache and later served to different users. Because Axum is a Rust web framework that encourages explicit handler composition and strongly typed routing, developers may assume that responses are automatically isolated per request context. This assumption can lead to caching logic that does not sufficiently differentiate requests, allowing an attacker to poison the cache by influencing cache keys with attacker-controlled inputs.
Consider an endpoint that caches HTTP responses based on the request path alone, without factoring in authentication state or tenant identifiers. In Rust, this might look like using a request header value directly as part of the cache key. If the cache is shared across users, a single request with a manipulated header could overwrite cached entries intended for others. For example, an ETag or Authorization header mistakenly included in the key enables an attacker to force cached responses containing sensitive data or modified content to be replayed to other users. This violates separation between users and can lead to information disclosure or incorrect behavior being served persistently.
Axum’s middleware and extractor system makes it straightforward to implement caching, but without careful design, it can inadvertently expose these cache-poisoning risks. Because Rust enforces memory and thread safety, the runtime behavior may appear stable while still serving logically corrupted cached data. Attack patterns such as HTTP response splitting or manipulation of cache-control headers can be leveraged to alter how responses are stored and retrieved. Since Axum applications often integrate with backend services or databases, poisoned cache entries may persist across deployments, amplifying the impact. The combination of high-performance routing in Rust and shared caching layers requires precise control over what constitutes a unique and trustworthy cache key.
Rust-Specific Remediation in Axum — concrete code fixes
To mitigate cache poisoning in Axum, explicitly define cache keys that incorporate only safe, non-user-influenced components and isolate responses by tenant or user context where necessary. Avoid using raw header values directly in cache keys. Instead, validate and sanitize inputs, and consider hashing normalized paths along with authorized context identifiers. Below are concrete Rust examples demonstrating secure caching patterns with Axum using async-trait and headers crates to safely handle typed headers.
Example 1: Safe cache key construction with tenant isolation
use axum::{routing::get, Router};
use headers::authorization::Bearer;
use headers::Authorization;
use std::net::SocketAddr;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
async fn compute_cache_key(path: &str, auth_header: Option<&Authorization>) -> u64 {
let mut hasher = DefaultHasher::new();
// Include only path and a sanitized tenant hint, never raw header values
path.hash(&mut hasher);
if let Some(auth) = auth_header {
// Use presence or a normalized tenant ID derived from auth, not the token itself
"authenticated".hash(&mut hasher);
} else {
"anonymous".hash(&mut hasher);
}
hasher.finish()
}
async fn handler(
Extension(cache): Extension>>>,
headers: HeaderMap,
) -> String {
let auth = headers.get::>();
let key = compute_cache_key("/api/data", auth.as_ref().ok()).await;
// Safe retrieval and storage using normalized key
// ...
}
fn main() {
let app = Router::new().route("/data", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
Example 2: Validating inputs before caching and avoiding header-based keys
use axum::{http::HeaderName, response::IntoResponse};
use std::collections::HashSet;
fn is_safe_cache_header(name: &HeaderName) -> bool {
let forbidden: HashSet<&str> = ["authorization", "cookie", "proxy-authorization"].iter().cloned().collect();
!forbidden.contains(name.as_str().to_lowercase().as_str())
}
async fn build_safe_key(headers: &HeaderMap, path: &str) -> Option {
let filtered: Vec<(&HeaderName, &str)> = headers.iter()
.filter(|(name, _)| is_safe_cache_header(name))
.filter_map(|(name, value)| value.to_str().ok().map(|v| (name, v)))
.collect();
// Use filtered, sanitized header values if necessary, or exclude them entirely
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.hash(&mut hasher);
for (name, value) in filtered {
name.as_str().hash(&mut hasher);
// Optionally normalize value, e.g., trim, lowercase, or map to enum
value.hash(&mut hasher);
}
Some(hasher.finish())
}
These examples emphasize excluding sensitive headers from cache keys, normalizing inputs, and isolating cache entries by authenticated context. In production, combine these patterns with time-based or size-bound eviction policies to reduce the window and impact of any poisoned entry.