Cache Poisoning in Rocket with Basic Auth
Cache Poisoning in Rocket with Basic Auth — how this specific combination creates or exposes the vulnerability
Cache poisoning occurs when an attacker manipulates a cache key so that a malicious response is stored and subsequently served to other users. In Rocket, using HTTP Basic Auth in combination with caching mechanisms can unintentionally enable cache poisoning when the cache key does not include the authorization context. Rocket applications often rely on request data such as headers, query parameters, and path segments to construct cache keys. If the authorization header is excluded from that key, an authenticated user’s cached representation can be reused for another user with different authorization context, leading to information disclosure or privilege confusion.
Consider a Rocket endpoint that caches responses for performance. If the route uses Basic Auth for access control but the caching layer uses only the request path and query string as the cache key, two different users with different credentials will share the same cached response. An attacker who can observe or influence one user’s request might cause a cache entry to be stored under their own credentials, later retrieving another user’s sensitive data when the cache is served. This becomes more likely when responses vary based on user-specific data that is not part of the cache key but is instead enforced only by Basic Auth checks that occur after cache lookup.
Basic Auth sends credentials in an encoded (not encrypted) header on every request. When caching proxies or in-memory caches are deployed without careful key design, the static nature of many cache keys collides with the dynamic nature of per-user authorization. Rocket does not automatically include the Authorization header in cache keys, so developers must explicitly ensure that user-specific or scope-specific identifiers are part of the cache key when building custom caching logic. Without this, an API that returns different representations based on identity but uses a path-only cache key will incorrectly share responses across users, violating separation between authenticated contexts.
SSRF and input validation checks within middleBrick highlight scenarios where cache poisoning could be discovered during scanning. For example, if a response includes sensitive data in JSON fields that should be user-specific, and that response is cached without considering authorization, the scan findings will point to missing input validation and authorization in cache behavior. Developers should correlate route guards like Basic Auth with caching implementations to ensure that cache keys incorporate user identity, tenant identifiers, or any factor that changes the expected response.
In Rocket, you can mitigate cache poisoning with Basic Auth by ensuring that any caching logic includes the user identity or scope in the cache key. This can be achieved by reading the Authorization header, decoding the username, and appending it to the cache key, or by scoping cache entries to a per-user or per-token namespace. MiddleBrick’s checks for Input Validation, Authorization (BOLA/IDOR), and Data Exposure help surface routes where cached data could be mismatched with authentication context, guiding developers to align caching strategy with access control.
Basic Auth-Specific Remediation in Rocket — concrete code fixes
To prevent cache poisoning when using Basic Auth in Rocket, you must incorporate the authenticated identity into cache keys and avoid caching responses that contain user-specific data unless the key reflects that user. Below are concrete code examples showing how to implement Basic Auth and build cache-aware routes in Rocket safely.
Example 1: Basic Auth with per-user cache key in Rocket
This example demonstrates extracting the Basic Auth credentials, decoding the username, and using it as part of a cache key. The approach ensures that cached entries are isolated per user, preventing one user from receiving another user’s cached response.
use rocket::http::Status;
use rocket::request::Request;
use rocket::response::Responder;
use rocket::{Build, Rocket, State};
use rocket_dyn_templates::Template;
use std::collections::HashMap;
use std::sync::Mutex;
// A simple in-memory cache for demonstration; in production, use a robust cache like Redis.
struct Cache {
store: Mutex>,
}
#[rocket::main]
async fn main() -> Result<_, rocket::Error> {
let cache = Cache {
store: Mutex::new(HashMap::new()),
};
let rocket = rocket::build()
.manage(cache)
.mount("/", routes![profile]);
rocket.launch().await
}
fn basic_auth_middleware(request: &Request) -> Option<(String, String)> {
request.headers().get_one("Authorization").and_then(|header| {
if header.starts_with("Basic ") {
let encoded = header.trim_start_matches("Basic ");
// In production, use proper base64 decoding and handle errors.
let decoded = general_purpose::STANDARD.decode(encoded).ok()?;
let creds = String::from_utf8(decoded).ok()?;
let parts: Vec<&str> = creds.splitn(2, ':').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
} else {
None
}
})
}
#[get("/profile")]
fn profile(cache: &State<Cache>) -> Result<String, Status> {
// Extract credentials from the request; in a real app, pass request guards.
// For this example, we assume a helper retrieves them.
// In practice, attach this logic to a request guard or fairing.
let user = "current_user"; // Placeholder: derive from Basic Auth
let cache_key = format!("profile:user:{}", user);
{
let store = cache.store.lock().unwrap();
if let Some(cached) = store.get(&cache_key) {
return Ok(cached.clone());
}
}
// Simulate fetching user-specific data
let response_data = format!("Profile for {}
", user);
let mut store = cache.store.lock().unwrap();
store.insert(cache_key, response_data.clone());
Ok(response_data)
}
Example 2: Using Rocket request guards to enforce auth before cache lookup
This snippet shows a more idiomatic Rocket approach using a custom request guard to validate Basic Auth and then using the authenticated username in the cache key. This keeps authorization and caching concerns separated and explicit.
use rocket::request::{self, FromRequest, Request};
use rocket::Outcome;
use rocket::http::Status;
use std::sync::Mutex;
struct User(String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = ();
async fn from_request(request: &Request<'r>) -> Outcome<Self, Self::Error> {
let headers = request.headers();
let auth = headers.get_one("Authorization");
match auth {
Some(header) if header.starts_with("Basic ") => {
let encoded = header.trim_start_matches("Basic ");
let decoded = general_purpose::STANDARD.decode(encoded).map_err(|_| ())?;
let creds = String::from_utf8(decoded).map_err(|_| ())?;
let mut parts = creds.splitn(2, ':');
match (parts.next(), parts.next()) {
(Some(username), Some(_password)) => Outcome::Success(User(username.to_string())),
_ => Outcome::Failure((Status::Unauthorized, ())),
}
}
_ => Outcome::Failure((Status::Unauthorized, ())),
}
}
}
// Route using the User guard; cache key includes username.
#[get("/data")]
fn user_data(user: User, cache: &State<Cache>) -> String {
let cache_key = format!("data:user:{}", user.0);
// Check cache and return or compute as shown earlier.
format!("Data for user {{}}", user.0)
}
Operational and architectural guidance
- Include the authenticated identity or a scoped token in your cache key whenever responses differ by user or role.
- Do not cache responses that contain private user data unless the cache key explicitly incorporates user identifiers.
- Validate and decode Basic Auth credentials using a robust base64 decoder and handle malformed inputs gracefully.
- Use middleware or request guards to centralize authentication logic, making it easier to reason about cache behavior and access control.
By aligning cache keys with the security context enforced by Basic Auth, you eliminate the conditions that enable cache poisoning. MiddleBrick scans can surface mismatches between authentication mechanisms and caching strategies, pointing you toward precise remediation steps such as adding identity to cache keys and validating input handling at the cache layer.