Cache Poisoning in Axum with Cockroachdb
Cache Poisoning in Axum with Cockroachdb — how this specific combination creates or exposes the vulnerability
Cache poisoning in the context of an Axum service backed by Cockroachdb occurs when an attacker causes cached responses to store attacker-controlled data, which are then served to other users. This typically arises when cache keys are derived from unvalidated or attacker-influenced inputs and the caching layer does not enforce strict isolation between tenants or users.
Axum does not include a built-in cache; if you introduce caching (for example via Redis, in-memory stores, or HTTP caches) and use Cockroachdb as the source of truth, the risk is in how you construct cache keys and validate inputs. A common pattern is to build a key from a user identifier or a request parameter that is directly concatenated with a static prefix, such as format!("user:{{user_id}}"). If the user_id is supplied by the client and not strictly validated, an attacker can manipulate it to overwrite entries used by other users or to inject malicious payloads that are later deserialized or rendered.
With Cockroachdb, the risk is exacerbated when application-level caching is implemented without considering row-level security or tenant isolation. For instance, if a cache key incorporates tenant identifiers that are not verified against the database’s tenant mapping, an attacker who can control the tenant identifier might read or write cached data belonging to another tenant. Additionally, if query results are cached based on raw SQL strings or ORM-generated queries that include unescaped identifiers, an attacker might influence the cache key to cause cache collisions or to poison entries with data retrieved via crafted queries.
Another vector involves response caching where the cache key does not sufficiently differentiate content by critical request dimensions such as user permissions or data sensitivity. An authenticated user might request a profile endpoint that returns private data; if the cache key omits the user context, a subsequent request by a different user could receive the previously cached private response. Because Cockroachdb is often used in distributed systems where read-after-write consistency expectations are strict, developers might rely on caching to reduce latency, inadvertently creating a scenario where stale or poisoned cache entries persist across privilege boundaries.
Real-world exploit patterns mirror those seen in general cache poisoning, such as those cataloged in the OWASP API Top 10, but the specifics of Axum’s request handling and Cockroachdb’s SQL semantics require careful design. For example, using string interpolation to construct cache keys without normalizing or restricting input types can lead to key ambiguity. Similarly, failing to enforce least privilege on database connections used by the caching layer can allow an attacker to leverage compromised application code to manipulate cached entries across users or tenants.
Cockroachdb-Specific Remediation in Axum — concrete code fixes
Remediation centers on strict input validation, canonical cache key construction, and tenant-aware isolation. Below are concrete Axum integration examples using a Cockroachdb connection pool with sqlx, demonstrating safe patterns.
- Validate and normalize identifiers before using them in cache keys or queries:
use axum::{routing::get, Router};
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPoolOptions;
use std::net::SocketAddr;
#[derive(Debug, Serialize, Deserialize)]
struct Profile {
user_id: i64,
display_name: String,
}
async fn get_profile(
user_id: String, // from path or query
pool: &sqlx::PgPool,
) -> Result {
// Canonicalize: parse and enforce constraints
let uid = user_id.parse::().map_err(|_| sqlx::Error::RowNotFound)?;
sqlx::query_as!(Profile, "SELECT user_id, display_name FROM profiles WHERE user_id = $1", uid)
.fetch_one(pool)
.await
}
#[tokio::main]
async fn main() {
let pool = PgPoolOptions::new()
.connect(&std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"))
.await
.unwrap();
let app = Router::new().route("/profiles/:user_id", get(|user_id: String, pool: axum::extract::State<sqlx::PgPool>| async move {
match get_profile(user_id, &pool).await {
Ok(p) => axum::Json(p),
Err(_) => axum::Json(Profile { user_id: -1, display_name: String::from("not_found") }),
}
}));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
- Use tenant-aware cache keys and enforce tenant ownership at the database layer:
use axum::routing::get;
use serde::{Deserialize, Serialize};
use sqlx::postgres::PgPoolOptions;
#[derive(Debug, Serialize, Deserialize)]
struct TenantProfile {
tenant_id: String,
user_id: i64,
display_name: String,
}
fn make_cache_key(tenant_id: &str, user_id: i64) -> String {
// Canonical key format; avoid ambiguous concatenations
format!("tenant:{tenant}:user:{user}", tenant = tenant_id, user = user_id)
}
async fn get_tenant_profile(
tenant_id: String,
user_id: String,
pool: &sqlx::PgPool,
) -> Result {
// Validate tenant_id format (e.g., UUID or alphanumeric pattern)
if !tenant_id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
return Err(sqlx::Error::RowNotFound);
}
let uid = user_id.parse::().map_err(|_| sqlx::Error::RowNotFound)?;
// Enforce tenant ownership in query
sqlx::query_as!(
TenantProfile,
"SELECT tenant_id, user_id, display_name FROM profiles WHERE tenant_id = $1 AND user_id = $2",
tenant_id,
uid
)
.fetch_one(pool)
.await
}
- Ensure that caching layers differentiate by security context and do not share keys across privilege levels:
// Example of namespacing cache keys by role
fn admin_cache_key(user_id: i64) -> String {
format!("admin:profile:user:{user_id}")
}
fn user_cache_key(user_id: i64) -> String {
format!("user:profile:user:{user_id}")
}
These patterns reduce the likelihood of cache key ambiguity and help ensure that cached responses cannot be reused across security boundaries. In combination with runtime input validation and strict schema constraints in Cockroachdb, they mitigate the primary causes of cache poisoning in an Axum service.