Cache Poisoning in Actix with Cockroachdb
Cache Poisoning in Actix with Cockroachdb — how this specific combination creates or exposes the vulnerability
Cache poisoning in the Actix web framework when paired with CockroachDB typically arises from caching responses that include user-specific or tenant-specific data without sufficient key differentiation. If an endpoint builds a cache key from only public parts of a request (for example, the path) and stores a response that contains authorization-derived or identity-derived data, an attacker can cause a victim to receive another user’s data through cache side-channels. This is a form of BOLA/IDOR that manifests at the caching layer rather than at the database query layer.
With CockroachDB, the concern is less about SQL injection and more about how application-level caching logic interacts with tenant identifiers, primary keys, and request context. Suppose an Actix handler reads a user_id from an authorization token, queries CockroachDB for profile data, and caches the result in Redis or an in-memory map using only the route path as the key. The same cached entry would be returned to any user who hits that path, effectively leaking one user’s profile to another. CockroachDB’s strong consistency does not prevent this; it simply means that every cache miss will reliably reflect the requester’s permissions, while a poisoned cache entry can persist across different users on subsequent cache hits.
A realistic scenario: an endpoint GET /api/v1/profile caches the JSON response keyed by /api/v1/profile. The handler queries CockroachDB with a tenant or user identifier taken from the request context. If the cache key does not incorporate the user identifier, attacker-controlled cache entries can be injected by tricking an authenticated user into requesting a crafted URL that includes an attacker-controlled identifier in a header or cookie that the handler mistakenly uses to form the cached payload. Subsequent requests by other users may then receive the attacker’s data. This maps to OWASP API Top 10 2023’s Broken Object Level Authorization and can also intersect with excessive data exposure if cached responses include sensitive fields.
Additional risk patterns include caching error responses that differ by user, or caching based on query parameters that do not uniquely and safely identify the requester. CockroachDB’s multi-region capabilities and serializable isolation do not mitigate these design issues; they only affect where and how data is stored. The key mitigation is to ensure cache keys are a cryptographic hash or concatenation that includes all context needed to differentiate users and tenants, and to avoid caching responses that contain sensitive or user-specific content unless the cache layer supports tenant-aware isolation.
Cockroachdb-Specific Remediation in Actix — concrete code fixes
To remediate cache poisoning in Actix with CockroachDB, tie cache keys to the requesting identity and tenant context, and avoid caching user-specific responses in shared caches. Below are concrete patterns you can apply in your Actix service.
1. Include user and tenant in cache key
Never use only the route path for cache keys when responses depend on authentication or tenant context. Incorporate a stable user identifier and, if applicable, a tenant or organization identifier.
use actix_web::{web, HttpResponse, HttpRequest};
use sha2::{Sha256, Digest};
async fn profile(
req: HttpRequest,
pool: web::Data,
redis_client: web::Data,
) -> HttpResponse {
let user_id = req.extensions().get::().cloned().unwrap_or_default();
let tenant_id = req.extensions().get::().cloned().unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(format!("profile:user:{}:tenant:{}", user_id, tenant_id));
let cache_key = format!("{:x}", hasher.finalize());
let mut conn = pool.get().expect("failed to get DB connection");
let cached = redis_client.get(&cache_key).ok().flatten();
if let Some(cached) = cached {
return HttpResponse::Ok().json(cached);
}
// CockroachDB query using the user-tenant context
let row = conn
.query_one(
"SELECT display_name, email FROM profiles WHERE user_id = $1 AND tenant_id = $2",
&[&user_id, &tenant_id],
)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let payload = serde_json::json!({
"display_name": row.get::<_, String>("display_name"),
"email": row.get::<_, String>("email"),
});
let _ = redis_client.set_ex(&cache_key, payload.to_string(), 300); // 5 min TTL
HttpResponse::Ok().json(payload)
}
2. Parameterized queries with strongly-typed IDs to prevent confusion attacks
Ensure query parameters are bound as typed values rather than interpolated, and validate that IDs used for caching are aligned with the authenticated identity.
async fn get_settings(
req: HttpRequest,
pool: web::Data,
redis_client: web::Data,
) -> HttpResponse {
let user_id: i64 = req.extensions().get().copied().unwrap_or_default();
let cache_key = format!("settings:user:{}", user_id);
if let Some(cached) = redis_client.get::<_, String>(&cache_key).ok().flatten() {
return HttpResponse::Ok().body(cached);
}
let mut conn = pool.get().expect("failed to get DB connection");
let row = conn
.query_one(
"SELECT theme, locale FROM user_settings WHERE user_id = $1",
&[&user_id],
)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
let settings = serde_json::json!({
"theme": row.get::<_, String>("theme"),
"locale": row.get::<_, String>("locale"),
});
let _ = redis_client.set_ex(&cache_key, settings.to_string(), 600);
HttpResponse::Ok().json(settings)
}
3. Avoid caching responses that contain user-specific headers or cookies
If your handler inspects headers such as X-Forwarded-For or cookies to decide query parameters, do not use those inputs naively as cache keys. Instead, normalize and scope them to the authenticated identity.
async fn safe_search(
req: HttpRequest,
pool: web::Data,
redis_client: web::Data,
web::Query(params): web::Query,
) -> HttpResponse {
let user_id = req.extensions().get::().cloned().unwrap_or_default();
// Do not use raw header values directly in cache key; bind to user context
let cache_key = format!("search:user:{}:q:{}", user_id, params.q);
if let Some(cached) = redis_client.get::<_, String>(&cache_key).ok().flatten() {
return HttpResponse::Ok().body(cached);
}
let mut conn = pool.get().expect("failed to get DB connection");
let rows = conn
.query(
"SELECT id, title FROM items WHERE owner_id = $1 AND title ILIKE $2",
&[&user_id, &format!("%{}%", params.q)],
)
.await
.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;
// build response ...
let _ = redis_client.set_ex(&cache_key, "[]", 300);
HttpResponse::Ok().body("[]")
}
4. Middleware or extractor for cache-aware request context
Use an extractor that enriches the request with tenant and user context before handlers run, ensuring cache keys can safely incorporate these values.
pub struct AuthContext {
pub user_id: i64,
pub tenant_id: String,
}
pub async fn auth_extractor(req: &HttpRequest) -> Result {
// validate token, fetch tenant, return AuthContext
Ok(AuthContext { user_id: 1, tenant_id: "acme".into() })
}
// In route registration
App::new()
.app_data(web::Data::new(redis_client))
.route("/api/v1/profile", web::get().to(profile))