Excessive Data Exposure in Axum with Dynamodb
Excessive Data Exposure in Axum with Dynamodb
Excessive Data Exposure occurs when an API returns more data than necessary for a given operation. In Rust services using the Axum web framework with Amazon DynamoDB as the backend, this commonly manifests through several patterns: returning entire DynamoDB items when only a subset of attributes is required, failing to filter sensitive fields like passwords or tokens before serialization, and exposing internal identifiers or metadata that should remain internal.
When an Axum handler deserializes a DynamoDB GetItem or Query response into a domain struct, developers might inadvertently retain all fields from the database record. For example, a user profile endpoint might fetch a DynamoDB item containing email, password_hash, api_key, and internal_role, then serialize the full struct as JSON to the client. This exposes credentials and administrative data that should never leave the server. The issue is compounded when the same serialization logic is reused across multiple endpoints without field-level filtering.
DynamoDB's schema-less design exacerbates this risk. Because items can have varying attributes, a handler might accept a generic HashMap<String, AttributeValue> or a loosely-typed structure and forward it directly to the response. Without explicit field selection or transformation, sensitive attributes such as ssn or payment_token can be returned to the client. MiddleBrick scans detect these patterns by correlating OpenAPI definitions with runtime behavior, identifying endpoints that return broad or unstructured data from DynamoDB without proper attribute filtering.
In Axum, serialization typically uses serde. If a struct includes all fields from a DynamoDB response and is returned directly from an endpoint, the JSON output will contain every mapped field. Consider a handler that retrieves a user item and returns User as JSON:
use axum::{routing::get, Router};use aws_sdk_dynamodb::Client;
use serde::Deserialize;async fn get_user(
client: &Client,
user_id: String,) -> Result<impl IntoResponse, (StatusCode, String)> {
let response = client.get_item()
.table_name("users")
.key("user_id", AttributeValue::S(user_id))
.send()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let user_item = response.item().ok_or_else(|| (StatusCode::NOT_FOUND, "User not found".to_string()))?;
// Deserialize into a strongly-typed struct
let user: User = serde_dynamo::from_attrs(user_item.clone()).map_err(|e| {
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
Ok(Json(user))
}
If the User struct includes fields like password_hash or api_key, this endpoint exposes them. A safer approach is to define a separate response DTO (Data Transfer Object) that includes only necessary fields:
use serde::Serialize;
#[derive(Serialize)]
struct UserPublic {
user_id: String,
email: String,
name: String,
}
Then transform the DynamoDB item into this limited structure before returning. This pattern ensures that sensitive attributes are never serialized, reducing the attack surface for data exposure.
Dynamodb-Specific Remediation in Axum
Remediation focuses on controlling which attributes are deserialized and returned. Instead of deserializing entire DynamoDB items into domain structs, use selective deserialization or projection expressions to retrieve only required attributes from DynamoDB. This reduces both network payload and memory exposure.
Define response structs that mirror only the intended public shape of your data. Use serde attributes to control serialization and ensure no sensitive fields are accidentally included:
use serde::Serialize;
#[derive(Serialize)]
pub struct UserResponse {
pub user_id: String,
pub email: String,
pub name: String,
}
In your handler, map the DynamoDB item to this restricted structure:
use aws_sdk_dynamodb::types::AttributeValue;
use serde_dynamo::from_attrs;
fn to_user_response(item: &std::collections::HashMap<String, AttributeValue>) -> Result<UserResponse, String> {
let user: UserResponse = from_attrs(item.clone()).map_err(|e| e.to_string())?;
Ok(user)
}
Alternatively, use projection expressions in get_item or query to fetch only needed attributes:
let response = client.get_item()
.table_name("users")
.key("user_id", AttributeValue::S(user_id))
.projection_expression("user_id, email, name")
.send()
.await;
This ensures DynamoDB returns only the specified attributes, preventing accidental exposure of other fields stored in the item.
When using query or scan operations, apply filter expressions on the server side and avoid returning full items. Combine this with proper error handling that does not leak internal details:
let response = client.scan()
.table_name("users")
.filter_expression("account_status = :status")
.expression_attribute_values(":status", AttributeValue::String("active".to_string()))
.select(Select::SpecificAttributes)
.projection_expression("user_id, email")
.send()
.await;
Finally, validate and sanitize all outputs. Even with projection, ensure that string fields do not contain unexpected sensitive content. Using dedicated response structs and avoiding generic deserialization into HashMap or serde_json::Value provides clearer boundaries and reduces the risk of exposing excessive data.
Related CWEs: propertyAuthorization
| CWE ID | Name | Severity |
|---|---|---|
| CWE-915 | Mass Assignment | HIGH |