Deserialization Attack in Axum (Rust)
Deserialization Attack in Axum with Rust — how this specific combination creates or exposes the vulnerability
A deserialization attack in an Axum service written in Rust typically occurs when an endpoint accepts an HTTP request body and deserializes it into a Rust data structure without strict validation. Axum does not perform any automatic schema validation on JSON payloads; it relies on the developer to choose how to parse the body. Using common deserialization crates such as serde_json or serde_urlencoded means that if the application defines permissive or complex types, an attacker can supply crafted payloads that trigger unexpected behavior. For example, deeply nested structures or polymorphic deserialization using untagged enums can cause excessive memory consumption or unintended variant construction, leading to denial of service or logic bypass. In an API designed for high throughput, these issues may not surface until under load, making them particularly dangerous in production. Because Axum is often used to build public-facing services, any endpoint that accepts serialized input must be treated as an attack surface. Attack patterns such as injection through untagged enums, exploitation of type confusion via serde’s representation, or resource exhaustion via large or recursive payloads are well documented in the OWASP API Security Top 10 and have associated CVEs in related libraries. Even when using strongly typed structures, missing field-level validation can allow malicious values to propagate into downstream logic. In a microservice architecture where services communicate over HTTP, an Axum endpoint that consumes serialized messages from other services can inadvertently propagate or amplify these issues. This is compounded when the deserialization logic is shared across services with differing trust boundaries. The use of middleware such as axum::extract::Json provides convenience but does not enforce schema constraints, placing the burden on the developer to implement checks. Understanding how serde’s deserialization rules interact with Axum’s extractor model is essential to avoid unintentionally exposing a deserialization vector. Security checks that validate the structure, size, and content of incoming serialized data before it reaches application logic are crucial. Tools that analyze API definitions and runtime behavior can identify these risks before deployment, helping teams enforce secure defaults and prevent common pitfalls associated with deserialization in Rust web services.
Rust-Specific Remediation in Axum — concrete code fixes
To mitigate deserialization risks in Axum with Rust, apply strict validation and limit what serde will accept. Prefer using serde with deny_unknown_fields and explicitly defined variants to reduce the risk of type confusion. For complex models, validate data after deserialization using a dedicated validation layer or types that enforce constraints, such as newtype wrappers with FromStr or custom validate methods. Avoid untagged enums for inputs from untrusted sources; prefer adjacently tagged or internally tagged representations with a strict set of known variants. Limit payload size at the HTTP layer by configuring a reasonable max_length for the JSON extractor or by using a middleware that rejects oversized bodies before deserialization. The following code examples demonstrate secure patterns for Axum endpoints.
Example 1: Strict JSON extraction with deny_unknown_fields
use axum::{routing::post, Router};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct CreateUser {
email: String,
#[serde(with = "serde_with::rust::double_option")]
age: Option
Example 2: Size-limited extractor and custom validation
use axum::extract::State;
use serde::Deserialize;
use std::net::SocketAddr;
#[derive(Debug, Deserialize)]
struct QueryRequest {
query_id: u64,
}
async fn handle_query(
State(config): State>,
// Limit JSON body size to 1 KiB before deserialization
axum::extract::Json,
) -> String {
// Validate semantic constraints after deserialization
if config.allowed_ids.contains(&query.query_id) {
"OK".to_string()
} else {
"Forbidden".to_string()
}
}
Example 3: Rejecting problematic polymorphic deserialization
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "data")]
enum SafeMessage {
Text { content: String },
Number { value: i64 },
}
// Do not use untagged enums for untrusted input
// #[derive(Deserialize)]
// enum Untagged { A(String), B { x: i32 } }
Operational practices
- Set reasonable limits on JSON payload size at the HTTP server or gateway level.
- Audit dependencies for known vulnerabilities, paying special attention to serde and related crates (e.g., CVE-2020-28669 in serde_json).
- Use static analysis tools such as cargo clippy and cargo audit as part of CI/CD.