Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
axum::Json deserialize request bodies while protecting against malicious payload sizes?axum::Json protects against malicious payload sizes through axum's layered defense system: the framework applies a default body size limit of 2MB before any extraction occurs, preventing unbounded memory allocation from attacker-controlled request bodies. When a request body exceeds this limit, axum rejects it with a 413 Payload Too Large response before the Json extractor even attempts deserialization. This protection operates at the request layer, not the JSON layerâthe body size limit applies to all extractors that consume the request body, ensuring that malicious payloads cannot exhaust server memory through oversized requests. The limit is configurable via DefaultBodyLimit for applications needing different thresholds, but the key insight is that protection happens before deserialization, not during it, which is critical because deserializing a 10GB malformed JSON string could crash the server before any validation logic runs.
use axum::{
Json,
Router,
routing::post,
extract::DefaultBodyLimit,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserData {
name: String,
email: String,
}
async fn create_user(Json(payload): Json<UserData>) -> String {
format!("Created user: {}", payload.name)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user));
// axum applies DefaultBodyLimit::default() automatically
// Default limit is 2MB (2,097,152 bytes)
// Requests larger than 2MB receive 413 Payload Too Large
// This protection applies BEFORE Json deserialization
// The body is limited, then Json parses it
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}axum enforces a 2MB body size limit by default before any extractor runs.
use axum::{
Json,
Router,
routing::post,
http::{StatusCode, Uri},
};
use serde::Deserialize;
#[derive(Deserialize)]
struct LargeData {
items: Vec<String>,
}
async fn process_data(Json(data): Json<LargeData>) -> &'static str {
"Data processed"
}
// The flow:
// 1. Client sends POST request with body
// 2. axum's request layer checks Content-Length header
// 3. If Content-Length > limit: immediate 413 response
// 4. If Content-Length <= limit: body is collected
// 5. If collected body > limit (for chunked encoding): stream cut off, 413 response
// 6. If body <= limit: passed to Json extractor
// 7. Json attempts deserialization
// 8. If deserialization fails: 400 Bad Request
// 9. If deserialization succeeds: handler receives parsed data
// Attackers cannot bypass this by:
// - Sending chunked encoding (body still counted)
// - Sending gzip content (Content-Length is post-decompression)
// - Removing Content-Length header (axum still limits collection)
fn main() {
println!("Protection flow demonstrated above");
}Protection happens at the HTTP layer, before JSON parsing.
use axum::{
Json,
Router,
routing::post,
extract::DefaultBodyLimit,
};
use serde::Deserialize;
use std::num::NonZeroUsize;
#[derive(Deserialize)]
struct FileUpload {
content: String,
}
async fn upload(Json(file): Json<FileUpload>) -> &'static str {
"File uploaded"
}
#[tokio::main]
async fn main() {
// Option 1: Use default 2MB limit
let app_default = Router::new()
.route("/upload", post(upload));
// Option 2: Set custom limit (e.g., 10MB)
let app_10mb = Router::new()
.route("/upload", post(upload))
.layer(DefaultBodyLimit::max(10 * 1024 * 1024));
// Option 3: Disable limit entirely (NOT RECOMMENDED for production)
let app_unlimited = Router::new()
.route("/upload", post(upload))
.layer(DefaultBodyLimit::disable());
// Option 4: Different limits per route
let app_per_route = Router::new()
.route("/small", post(upload).layer(DefaultBodyLimit::max(1024))) // 1KB
.route("/large", post(upload).layer(DefaultBodyLimit::max(100 * 1024 * 1024))); // 100MB
// The limit is enforced before your handler runs
// Attackers cannot bypass it by crafting malicious JSON
}Configure limits based on your application's expected payload sizes.
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct NestedData {
// Attackers can craft deeply nested JSON
// that causes stack overflow during deserialization
nested: Option<Box<NestedData>>,
}
#[derive(Deserialize)]
struct ArrayData {
// Attackers can send arrays with millions of elements
// causing memory exhaustion
items: Vec<String>,
}
// Attack vectors:
//
// 1. Memory exhaustion
// - Send 10GB JSON array
// - Without limits: server allocates 10GB, crashes
// - With limits: rejected with 413 before allocation
//
// 2. Deeply nested structures
// - Send 10,000 levels of nesting
// - Can cause stack overflow in serde
// - Size limit helps but doesn't fully prevent this
//
// 3. Compressed payloads
// - Send highly compressed data that expands 1000x
// - Size limit counts decompressed size
// - Prevents zip bomb attacks
fn main() {
println!("Attack vectors documented above");
}Size limits prevent memory exhaustion from attacker-controlled payloads.
use axum::{
Json,
Router,
routing::post,
extract::{DefaultBodyLimit, rejection::JsonRejection},
http::StatusCode,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct RequestData {
name: String,
}
// Layered defense:
//
// Layer 1: HTTP Layer (DefaultBodyLimit)
// - Checks Content-Length header
// - Limits body collection
// - Returns 413 if exceeded
// - Protects against: Memory exhaustion
//
// Layer 2: JSON Layer (Json extractor)
// - Parses JSON from limited body
// - Validates JSON syntax
// - Returns 400 if invalid JSON
// - Protects against: Malformed input
//
// Layer 3: Schema Layer (serde Deserialize)
// - Validates structure matches expected type
// - Returns 400 if schema mismatch
// - Protects against: Type confusion
//
// Layer 4: Application Layer (your validation)
// - Business logic validation
// - Returns 400/422 if validation fails
// - Protects against: Invalid data
async fn protected_handler(
result: Result<Json<RequestData>, JsonRejection>
) -> Result<String, StatusCode> {
match result {
Ok(Json(data)) => {
// Layer 4: Application validation
if data.name.len() > 100 {
return Err(StatusCode::BAD_REQUEST);
}
Ok(format!("Hello, {}", data.name))
}
Err(JsonRejection::JsonDataError(e)) => {
// Invalid JSON syntax
Err(StatusCode::BAD_REQUEST)
}
Err(JsonRejection::JsonSyntaxError(e)) => {
// JSON parse error
Err(StatusCode::BAD_REQUEST)
}
Err(JsonRejection::MissingJsonContentType(_)) => {
// Wrong Content-Type header
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE)
}
Err(_) => {
// Other errors
Err(StatusCode::BAD_REQUEST)
}
}
}
fn main() {
println!("Layered defense demonstrated above");
}Multiple protection layers ensure safety at each processing stage.
use axum::{
Json,
Router,
routing::post,
extract::DefaultBodyLimit,
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserData {
name: String,
}
async fn create_user(Json(data): Json<UserData>) -> impl IntoResponse {
format!("Created: {}", data.name)
}
// When body exceeds limit, axum returns 413 Payload Too Large
// You can customize this response:
async fn handle_413() -> impl IntoResponse {
(
StatusCode::PAYLOAD_TOO_LARGE,
Json(serde_json::json!({
"error": "payload_too_large",
"message": "Request body exceeds maximum allowed size",
"max_size_bytes": 2_097_152
}))
)
}
#[tokio::main]
async fn main() {
// You cannot directly catch 413 in your handler
// because the request is rejected before reaching it
//
// To customize 413 response, you need middleware or
// a custom extractor
// Using a custom layer:
let app = Router::new()
.route("/users", post(create_user))
.layer(DefaultBodyLimit::max(1024));
// Requests > 1KB receive 413
// The 413 is automatic - no handler code needed
}413 responses are automatic for oversized payloads.
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct Container {
// What happens with these?
// Case 1: Large array
items: Vec<String>, // Each string can be large
// Case 2: Deeply nested
nested: NestedContainer,
}
#[derive(Deserialize)]
struct NestedContainer {
level: u32,
child: Option<Box<NestedContainer>>,
}
// axum's protection:
//
// 1. Total body size is limited (DefaultBodyLimit)
// - A 2MB limit means the JSON string is at most 2MB
// - Deserializing into Vec<String> can't exceed reasonable memory
//
// 2. serde's string allocation is bounded by input size
// - Can't allocate more than input bytes for strings
// - A 2MB JSON can produce at most ~2MB of strings
//
// 3. What about nesting depth?
// - serde has recursion limits
// - Very deep nesting can still cause stack overflow
// - This is separate from body size
fn main() {
// Body size limit protects against:
// - Sending 10GB JSON to exhaust memory
// - Allocating huge strings/arrays
//
// Body size limit does NOT protect against:
// - Deeply nested JSON (stack overflow)
// - Application-level DoS (valid but slow processing)
// - Resource exhaustion in your handler
println!("Protection scope documented above");
}Body size limits bound memory allocation but don't protect against all attack vectors.
use axum::Json;
use serde::Deserialize;
#[derive(Deserialize)]
struct RequestData {
values: Vec<String>,
}
// NAIVE APPROACH (vulnerable):
//
// async fn vulnerable_handler(body: String) -> Result<String, Error> {
// // No size limit - body can be 10GB
// let data: RequestData = serde_json::from_str(&body)?;
// // If body is huge, this line crashes server
// Ok(format!("Received {} items", data.values.len()))
// }
//
// Problems:
// 1. No body size limit - attacker sends 10GB
// 2. Server allocates 10GB for String
// 3. Server potentially crashes
// 4. Other requests fail due to memory pressure
// AXUM'S APPROACH (protected):
//
// async fn protected_handler(Json(data): Json<RequestData>) -> String {
// // DefaultBodyLimit already enforced
// // body is guaranteed <= 2MB
// // deserialization won't exhaust memory
// format!("Received {} items", data.values.len())
// }
//
// Protections:
// 1. Body size limited before deserialization
// 2. Large requests rejected with 413
// 3. Handler only runs for valid-sized requests
// 4. Memory usage is bounded
fn main() {
println!("Comparison documented above");
}axum's protection operates before deserialization, preventing memory exhaustion.
use axum::Json;
use serde::Deserialize;
use std::sync::Arc;
#[derive(Deserialize)]
struct DeepNest {
// Attackers might try: {"level":1,"child":{"level":2,"child":{...}}}
// With thousands of nesting levels
level: u32,
child: Option<Box<DeepNest>>,
}
// Body size limit doesn't fully protect against deep nesting
// because 2MB can contain very deep JSON
// Additional protections:
//
// 1. serde_json has a default recursion limit
// - Prevents stack overflow from deep nesting
// - Returns error if nesting too deep
//
// 2. Application-level validation
// - Check depth in your data structures
// - Reject unreasonably deep nesting
fn main() {
// The 2MB limit protects memory
// serde_json's recursion limit protects the stack
// Your validation protects business logic
println!("Multiple protections needed for security");
}Defense in depth: body limits for memory, serde limits for recursion.
use axum::{
Json,
Router,
routing::{get, post},
extract::DefaultBodyLimit,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct Profile {
name: String,
bio: String,
}
#[derive(Deserialize)]
struct FileUpload {
filename: String,
content: String,
}
#[tokio::main]
async fn main() {
// Different endpoints have different size requirements
let app = Router::new()
// Small text data: 10KB limit is fine
.route("/profile", post(update_profile))
.layer(DefaultBodyLimit::max(10 * 1024))
// File uploads might need more
.route("/upload", post(upload_file))
.layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB
// GET requests don't need body limits (no body)
.route("/status", get(status));
// Best practices:
// 1. Set limits based on expected data size
// 2. Use smaller limits for text-only endpoints
// 3. Consider rate limiting for upload endpoints
// 4. Monitor memory usage in production
// 5. Log 413 responses for security analysis
}
async fn update_profile(Json(profile): Json<Profile>) -> &'static str {
"Profile updated"
}
async fn upload_file(Json(file): Json<FileUpload>) -> &'static str {
"File uploaded"
}
async fn status() -> &'static str {
"OK"
}Right-size limits based on endpoint requirements.
use axum::{
Json,
Router,
routing::post,
extract::DefaultBodyLimit,
http::{StatusCode, Request, header::CONTENT_TYPE},
body::Body,
};
use serde::Deserialize;
use tower::ServiceExt;
#[derive(Deserialize)]
struct TestData {
message: String,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/test", post(|Json(data): Json<TestData>| async move {
format!("Received: {}", data.message)
}))
.layer(DefaultBodyLimit::max(100)); // 100 byte limit
// Test: Small request succeeds
let small_request = Request::builder()
.method("POST")
.uri("/test")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(r#"{"message":"hi"}"#))
.unwrap();
let response = app.clone().oneshot(small_request).await.unwrap();
println!("Small request status: {}", response.status());
// 200 OK
// Test: Large request fails
let large_body = format!(r#"{{"message":"{}"}}"#, "x".repeat(200));
let large_request = Request::builder()
.method("POST")
.uri("/test")
.header(CONTENT_TYPE, "application/json")
.body(Body::from(large_body))
.unwrap();
let response = app.oneshot(large_request).await.unwrap();
println!("Large request status: {}", response.status());
// 413 Payload Too Large
}Test size limits to verify protection works correctly.
Defense layers:
| Layer | Protection | Enforced By |
|-------|------------|-------------|
| HTTP Body Size | Memory exhaustion | DefaultBodyLimit (2MB default) |
| JSON Syntax | Malformed data | Json extractor |
| Schema Validation | Type mismatch | serde deserialization |
| Business Rules | Invalid values | Application logic |
How size limits work:
| Request | Content-Length > Limit | Body Collected > Limit | Within Limit | |---------|----------------------|----------------------|-------------| | Result | 413 immediate | 413 during read | JSON parsed |
Key insight: axum::Json's protection against malicious payload sizes is fundamentally architecturalâit relies on axum's request processing pipeline to enforce size limits before any deserialization occurs. The Json extractor itself doesn't check sizes; it assumes the body has already been bounded by DefaultBodyLimit. This separation of concerns is deliberate: size limiting happens once at the HTTP layer and protects all body-consuming extractors (Json, Form, Bytes, etc.) without each extractor needing to reimplement limits. The 2MB default is a reasonable balance for typical JSON APIsâlarge enough for legitimate requests but small enough to prevent memory exhaustion attacks. Applications handling file uploads or large data sets should explicitly configure higher limits, but only on specific routes. The critical security property is that the limit is enforced before your handler runs: even if you forget to validate size in application code, the framework has already rejected oversized payloads with 413. This prevents a class of DoS attacks where untrusted input size could crash the server, making axum's default behavior safe by default rather than requiring explicit configuration to be secure.