Cache Poisoning in Axum with Dynamodb
Cache Poisoning in Axum with Dynamodb — how this specific combination creates or exposes the vulnerability
Cache poisoning in the context of an Axum service that uses DynamoDB as a persistence layer occurs when an attacker causes cached responses to store and later serve malicious or incorrect data to other users. Because Axum handlers typically query DynamoDB to build response payloads, unchecked inputs can shape both the cache key and the cached data. If the application uses request parameters, headers, or authenticated identifiers to form cache keys without strict validation, an attacker can force distinct poisoned entries. For example, a user ID taken directly from a header and used in a DynamoDB query key can lead to one user’s cached data being overwritten with another user’s data, effectively an Insecure Direct Object Reference (IDOR) combined with cache pollution.
With DynamoDB, this risk is amplified when caching logic stores entire query results or items keyed by user-controlled values. Suppose an endpoint accepts a query parameter team_id to fetch team details and caches the DynamoDB response keyed by team_id. An attacker supplying a different user’s team ID can poison the cache so that subsequent legitimate requests for that team receive the attacker’s data. Because the cache is often shared across requests, this can lead to horizontal privilege escalation where one user sees another’s data. The unauthenticated attack surface of Axum endpoints using DynamoDB increases the likelihood that poisoned entries persist across multiple users and requests.
Patterns that increase exposure include missing validation on string identifiers, missing canonicalization of query parameters, and missing normalization of JSON field ordering before caching. If Axum responses include sensitive fields derived from DynamoDB items, such as roles or permissions, and those fields are not validated before being stored in cache, the poisoned cache can propagate privilege escalation or data exposure. The vulnerability is not in DynamoDB itself but in how Axum builds cache keys and decides what to cache from DynamoDB responses, especially when those caches are shared and long-lived.
Dynamodb-Specific Remediation in Axum — concrete code fixes
To mitigate cache poisoning when using DynamoDB with Axum, enforce strict input validation, isolate cache entries by user and tenant, and avoid caching sensitive or user-specific data unless properly scoped. Below are concrete code examples that demonstrate safe patterns for Axum handlers using the official AWS SDK for Rust.
1) Validate and sanitize all inputs used in DynamoDB keys and cache keys. Ensure identifiers conform to an allowlist or strict pattern before using them in queries or cache keys.
use axum::{routing::get, Router};
use aws_sdk_dynamodb::Client;
use serde::Deserialize;
use std::net::SocketAddr;
use validator::Validate;
#[derive(Deserialize, Validate)]
struct TeamParams {
#[validate(length(min = 1, max = 64, message = "team_id must not be empty"))]
#[validate(regex = "^team_[a-z0-9]{1,32}$", message = "team_id format invalid")]
team_id: String,
}
async fn get_team(
params: axum::extract::Query,
client: axum::extract::State<Client>,
) -> Result<impl axum::response::IntoResponse, (axum::http::StatusCode, String)> {
params.validate().map_err(|e| (axum::http::StatusCode::BAD_REQUEST, e.to_string()))?;
let item = client
.get_item()
.table_name("teams")
.key("team_id", aws_sdk_dynamodb::types::AttributeValue::S(params.team_id.clone()).into())
.send()
.await
.map_err(|err| (axum::http::StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
// process item…
Ok(()) // placeholder
}
fn app() -> Router {
let client = Client::new(&aws_config::load_from_env().await);
Router::new().route("/team", get(get_team)).with_state(client)
}
2) Scope cache keys by tenant and user to prevent cross-user pollution. Use a composite key that includes a tenant identifier derived from authentication, never from raw user input alone.
use axum::{async_trait, extract::FromRequestParts};
use http::request::Parts;
struct AuthedUser {
user_id: String,
tenant_id: String,
}
#[axum::async_trait]
impl FromRequestParts<S> for AuthedUser {
type Rejection = (axum::http::StatusCode, String);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Extract token and validate; derive tenant from token claims
let user_id = "user-123".to_string();
let tenant_id = "tenant-abc".to_string();
Ok(AuthedUser { user_id, tenant_id })
}
}
async fn scoped_cache_key(authed: AuthedUser, team_id: String) -> String {
format!("team:{}:tenant:{}:user:{}", team_id, authed.tenant_id, authed.user_id)
}
3) Do not cache sensitive fields from DynamoDB. When storing responses, exclude or encrypt fields like roles, permissions, or PII. If caching is required, store only non-sensitive aggregates and re-derive authorization at runtime.
use serde_json::json;
fn sanitize_for_cache(item: &aws_sdk_dynamodb::types::Item) -> serde_json::Value {
// Assume `item` contains `team_name` and `role`; we drop role before caching
let obj = item_to_json(item);
json!({
"team_name": obj["team_name"],
// role omitted intentionally to avoid privilege escalation via cache poisoning
})
}
4) Use strong ETags or hashes for validation to ensure cached entries are not silently replaced by attacker-controlled values. Combine cache-key isolation with integrity checks so that poisoned entries are less likely to overwrite legitimate data.