HIGH cache poisoningactixdynamodb

Cache Poisoning in Actix with Dynamodb

Cache Poisoning in Actix with Dynamodb — how this specific combination creates or exposes the vulnerability

Cache poisoning occurs when an attacker manipulates cached responses so that malicious content is served to other users. In an Actix web service that uses DynamoDB as a backend data store, this typically arises when query parameters or headers that should not influence cache keys are included in the cache decision, or when responses from DynamoDB are stored and later reused without proper validation.

Consider an Actix handler that retrieves user profile data from DynamoDB based on a user_id path parameter and an optional view query parameter. If the application caches the DynamoDB response keyed only by user_id, but the DynamoDB query changes based on view, two different logical responses may be stored under the same cache entry. An attacker can request a benign view to poison the cache with a modified response, and subsequent requests for a different view may receive the tainted data.

DynamoDB-specific factors that enable or expose cache poisoning include:

  • Consistent read behavior: If an application caches successful GetItem or Query responses and later relies on cached entries for authorization checks (e.g., tenant or scope), an attacker who can control part of the request (such as a header or query parameter that does not affect the DynamoDB key) may cause the application to serve a cached response that incorrectly grants access.
  • Conditional expressions and sparse indexes: Using a DynamoDB conditional write or querying a Global Secondary Index (GSI) with varying filter expressions can yield different logical results for the same partition key. If the Actix caching layer does not incorporate filter expressions or attribute values into the cache key, the wrong response may be served.
  • DynamoDB Streams and eventual consistency: If cache invalidation is based on DynamoDB Streams, an attacker may exploit timing differences between stream processing and cache expiration to serve stale or maliciously altered content during the window between write and invalidation.

A realistic example is an Actix endpoint that caches user preferences fetched from DynamoDB. The endpoint uses a composite key of user ID and locale, but the cache key omits locale. An attacker can set a locale that triggers a maliciously shaped response (e.g., a JSON field used in an unsafe way by the client), which then gets cached and served to users of other locales.

To detect this risk, scans such as those provided by middleBrick examine how caching decisions incorporate request attributes and DynamoDB query parameters. The scanner checks whether cache keys account for all inputs that change the meaning of the DynamoDB response, including headers, query parameters, and payload-derived selectors.

Dynamodb-Specific Remediation in Actix — concrete code fixes

Remediation focuses on ensuring cache keys uniquely reflect all inputs that change the meaning of the DynamoDB response. In Actix, this means incorporating relevant parts of the request and DynamoDB query into the cache key and avoiding caching of responses that depend on authorization or tenant context unless the cache key includes the tenant identifier.

Below are concrete Actix examples using the official AWS SDK for Rust (aws-sdk-dynamodb). These snippets show how to build safe cache keys and avoid leaking cached data across users or views.

1. Include all request dimensions in the cache key

Ensure locale, view, or tenant are part of the cache key so distinct logical responses are not overwritten.

use actix_web::{web, HttpResponse};
use aws_sdk_dynamodb::Client;
use std::collections::HashMap;

async fn get_user_profile(
    path: web::Path<(String,)>,           // user_id
    query: web::Query>,
    dynamodb: web::Data,
) -> HttpResponse {
    let user_id = path.0;
    let view = query.get("view").map(|s| s.as_str()).unwrap_or("default");
    let locale = query.get("locale").map(|s| s.as_str()).unwrap_or("en");

    // Build a cache key that includes user_id, view, and locale
    let cache_key = format!("profile:{}:view={}:locale={}", user_id, view, locale);

    // pseudo-cache lookup; replace with your actual cache implementation
    if let Some(cached) = CACHE.get(&cache_key) {
        return HttpResponse::Ok().body(cached.clone());
    }

    let resp = dynamodb
        .get_item()
        .table_name("UserProfiles")
        .key("user_id", aws_sdk_dynamodb::types::AttributeValue::S(user_id.clone()))
        .send()
        .await;

    match resp {
        Ok(output) => {
            let body = serde_json::to_string(&output).unwrap_or_default();
            CACHE.insert(cache_key, body.clone());
            HttpResponse::Ok().body(body)
        }
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

2. Do not cache responses that depend on authorization attributes

Avoid caching responses that contain tenant-specific or scope-specific data unless the cache key includes the tenant identifier. If you must cache per-user data, include user or tenant in the key.

use actix_web::HttpRequest;

fn tenant_cache_key(req: &HttpRequest, base: &str) -> String {
    let tenant = req.headers().get("X-Tenant-ID")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("public");
    format!("{}:tenant={}", base, tenant)
}

async fn get_shared_config(
    req: HttpRequest,
    dynamodb: web::Data,
) -> HttpResponse {
    let key = tenant_cache_key(&req, "config:app");
    if let Some(cached) = CACHE.get(&key) {
        return HttpResponse::Ok().body(cached.clone());
    }

    let resp = dynamodb
        .get_item()
        .table_name("AppConfig")
        .key("config_key", aws_sdk_dynamodb::types::AttributeValue::S("global".to_string()))
        .send()
        .await;

    match resp {
        Ok(output) => {
            let body = serde_json::to_string(&output).unwrap_or_default();
            CACHE.insert(key, body.clone());
            HttpResponse::Ok().body(body)
        }
        Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
    }
}

3. Validate and normalize inputs before using them in DynamoDB queries

Normalize query parameters used in the request to prevent cache key mismatches caused by encoding differences or redundant parameters.

use url::form_urlencoded;

fn normalize_view(view: Option<&str>) -> String {
    view.map(|v| v.to_lowercase().trim().to_string())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| "default".to_string())
}

// In handler:
let view = normalize_view(query.get("view").map(String::as_str));
let cache_key = format!("profile:{}:view={}", user_id, view);

4. Prefer strongly-typed query extraction to avoid parameter confusion

Use a deserialization library (e.g., serde_qs) to ensure only intended parameters affect behavior and cache identity.

use serde::Deserialize;

#[derive(Deserialize)]
struct ProfileQuery {
    view: Option,
    locale: Option,
}

async fn get_user_profile_typed(
    path: web::Path<(String,)>,
    query: web::Query,
    dynamodb: web::Data,
) -> HttpResponse {
    let user_id = path.0;
    let view = query.view.as_deref().unwrap_or("default");
    let locale = query.locale.as_deref().unwrap_or("en");
    let cache_key = format!("profile:{}:view={}:locale={}", user_id, view, locale);
    // ... rest as above
}

Frequently Asked Questions

How can I test if my Actix + DynamoDB endpoint is vulnerable to cache poisoning?
Use a scanner like middleBrick that inspects how cache keys are built relative to request attributes and DynamoDB query parameters. You can also manually test by sending requests with different views or headers that do not affect the DynamoDB key and checking whether cached responses differ per request dimension.
Does DynamoDB’s native caching or DAX change cache poisoning risks in Actix?
DynamoDB native caching or DAX operate server-side and do not change application-level cache poisoning risks. If your Actix application caches responses based on incomplete keys, poisoned entries can still be served regardless of the backend caching behavior.