HIGH axumrustdeserialization attack

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>,
}

async fn create_user_handler(Json(payload): axum::extract::Json) -> String {
    // Additional application-level validation should occur here
    format!("User email: {}", payload.email)
}

fn app() -> Router {
    Router::new().route("/users", post(create_user_handler))
}

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.

Frequently Asked Questions

Does Axum automatically protect against deserialization attacks?
No, Axum does not perform schema validation or size limiting on its own. You must explicitly apply serde configurations such as deny_unknown_fields and validate payloads before using them in business logic.
What are the most critical mitigations for deserialization in Rust web APIs?
Use strongly typed structs with deny_unknown_fields, avoid untagged enums for external input, limit JSON body size, validate data after deserialization, and regularly audit dependencies for vulnerabilities such as CVE-2020-28669.