HIGH side channel attackaxumdynamodb

Side Channel Attack in Axum with Dynamodb

Side Channel Attack in Axum with Dynamodb — how this specific combination creates or exposes the vulnerability

A side channel attack in an Axum service that uses DynamoDB does not exploit a flaw in DynamoDB itself, but rather leaks information through timing or error behavior in how Axum handles requests and responses. In this combination, an attacker can infer existence of items or properties by observing differences in response times or error messages, even when the API enforces authentication and uses parameterized queries.

Consider an endpoint like /users/{user_id} implemented in Axum that performs a GetItem on DynamoDB. If the application returns a generic 404 Not Found for both missing items and missing permissions, but takes measurably longer to build the response when an item exists (because DynamoDB returns data), an attacker can perform a timing side channel. By measuring response times across many requests with different IDs, the attacker distinguishes between found and not-found cases, gradually mapping valid user IDs without ever seeing the data itself.

In DynamoDB, this can be exacerbated by conditional writes or queries that trigger different code paths. For example, an Axum handler that first calls GetItem to check existence, then conditionally performs a UpdateItem, introduces timing differences between the existence check succeeding and failing. If error handling for DynamoDB conditional check failures returns distinct HTTP status codes or response bodies (e.g., 409 vs 404), the attacker gains further discrimination. The attack surface is not limited to user enumeration; it can extend to sensitive attributes (e.g., presence of a ssn attribute) when conditional expressions or sparse index structures change response timing or error signatures.

Another vector involves DynamoDB Streams and event processing within Axum. If an Axum service consumes stream records and processes them differently based on attribute presence, observable delays or dropped connections while processing certain records can leak information about the record content. Similarly, unauthenticated endpoints that expose DynamoDB metadata (such as table ARNs in error traces) can aid an attacker in crafting targeted timing probes.

To contextualize within the broader security checks middleBrick runs, a side channel related to timing or error disclosure would typically surface under the Authentication and Property Authorization checks, alongside findings from the Data Exposure and Input Validation checks. middleBrick’s scans do not probe timing channels directly, but they validate error handling consistency and input validation robustness, which are prerequisites to mitigating side channel risks.

Dynamodb-Specific Remediation in Axum — concrete code fixes

Remediation focuses on making Axum’s interaction with DynamoDB constant-time where feasible and ensuring error paths do not leak distinguishable information. Below are concrete Axum handler examples using the official AWS SDK for Rust (aws-sdk-dynamodb).

1. Use a single, constant-time path for existence and data retrieval

Instead of first checking existence and then fetching, fetch once and handle missing items uniformly. This reduces timing variance between allowed and forbidden requests.

use aws_sdk_dynamodb::Client;
use axum::{response::IntoResponse, Json};
use serde::Deserialize;

#[derive(Deserialize)]
struct UserParams {
    user_id: String,
}

async fn get_user_handler(
    Json(params): Json,
    client: &Client,
    table_name: &str,
) -> impl IntoResponse {
    let output = client.get_item()
        .table_name(table_name)
        .set_key(Some({
            use aws_sdk_dynamodb::types::AttributeValue as AV;
            std::collections::HashMap::from([
                ("id".to_string(), AV::S(params.user_id.clone())),
            ])
        }))
        .send()
        .await;

    match output {
        Ok(o) => {
            if let Some(item) = o.item {
                // Process item; ensure no early returns that differ in timing from the missing case
                Json(serde_json::json!({ "user": item })).into_response()
            } else {
                // Consistent 404 response for missing item or missing access
                (axum::http::StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not_found" }))).into_response()
            }
        }
        Err(e) => {
            // Use a generic error response; log details server-side to avoid information leakage
            (axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "internal_error" }))).into_response()
        }
    }
}

2. Normalize responses for conditional writes

When using conditional expressions, treat check failures the same as missing items to avoid distinguishable error responses.

use aws_sdk_dynamodb::types::ConditionalCheckFailedException;

async fn update_user_if_exists(
    client: &Client,
    table_name: &str,
    user_id: &str,
) -> Result<(), (axum::http::StatusCode, Json<serde_json::Value>) > {
    let result = client.update_item()
        .table_name(table_name)
        .key("id", aws_sdk_dynamodb::types::AttributeValue::S(user_id.to_string()))
        .update_expression("SET #st = :val")
        .expression_attribute_names("#st", "status")
        .expression_attribute_values(":val", aws_sdk_dynamodb::types::AttributeValue::Bool(true))
        .condition_expression("attribute_exists(id)")
        .send()
        .await;

    match result {
        Ok(_) => Ok(()),
        Err(e) if e.is_conditional_check_failed_exception() =>
            Err((axum::http::StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "not_found" })))),
        Err(e) => {
            // Log `e` internally; return generic error to the caller
            Err((axum::http::StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "internal_error" }))))
        }
    }
}

3. Standardize error shapes and status codes

Ensure that error responses do not include stack traces or DynamoDB-specific messages that can be used to distinguish failure modes. Use middleware in Axum to sanitize error payloads before sending them to the client.

use axum::{http::StatusCode, response::Response};

fn sanitize_error_response(err: &aws_sdk_dynamodb::Error) -> (StatusCode, Json<serde_json::Value>) {
    // Log full error for debugging; return generic payload to client
    tracing::error!(error = %err, "DynamoDB error");
    (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "internal_error" })))
}

4. Avoid metadata leakage in logs and responses

Be cautious not to include request IDs or table ARNs in client-facing error messages. Configure logging to redact sensitive context and ensure that retries do not amplify timing differences.

Frequently Asked Questions

Can a side channel attack over timing differences work through multiple network hops?
Yes, timing side channels can work across network hops if the timing differences are consistent and measurable. Mitigations include constant-time processing on the server and introducing jitter in non-sensitive paths to reduce distinguishability.
Does enabling DynamoDB encryption at rest or in transit prevent side channel attacks?
No. Encryption at rest and in transit protect data confidentiality in storage and transit, but they do not affect timing or error-based side channels. Those must be addressed at the application and API design level.