Skip to content
rusthacks.dev
Security

Denial of Service Through Rust Parsers and Resource Limits

A parser that accepts any input can be made to use unbounded memory, time or stack. Set limits on size, depth and time, and know which crates already have them.

3 min read
Summarize in AI
Flat illustration for the post “Denial of Service Through Rust Parsers and Resource Limits”, composed around memory.

A request with a 3 MB body, 100,000 levels of nested brackets or a gzip stream that expands to 4 GB is perfectly legal bytes. Memory safety does not help, because the problem is not corruption: it is that the program did exactly what you wrote with input much bigger than you expected. This post lists six resource limits and how to apply each in a Rust service. It is part of the input risks in security for Rust developers.

Rust 1.90, Axum 0.8.

1. Size

Reading a body into memory without a cap lets one client allocate arbitrarily. Axum's Json, Form and Bytes extractors apply a 2 MB default. Set it explicitly where you need something else:

rust
use axum::{extract::DefaultBodyLimit, routing::post, Router};

fn app() -> Router {
    Router::new()
        .route("/upload", post(|body: axum::body::Bytes| async move { body.len().to_string() }))
        .layer(DefaultBodyLimit::max(1024 * 1024))
}

For streamed bodies, wrap with tower_http::limit::RequestBodyLimitLayer, which enforces the limit as bytes arrive and rejects requests that declare more in Content-Length. Also cap the number of headers and their sizes in the server config.

2. Depth

rust
fn depth(v: &serde_json::Value) -> usize {
    match v {
        serde_json::Value::Array(a) => 1 + a.iter().map(depth).max().unwrap_or(0),
        serde_json::Value::Object(o) => 1 + o.values().map(depth).max().unwrap_or(0),
        _ => 0,
    }
}

This function is itself recursive, and calling it on an adversarial value would overflow the stack, but serde_json refuses to parse beyond depth 128 by default ("recursion limit exceeded"), so the value cannot get that deep. Your own recursive-descent parsers need a depth counter; stack overflows abort the process and cannot be caught (debugging a stack overflow).

3. Expansion

Decompression, XML entity expansion and archive extraction can multiply input size. Limit the output, not just the input:

rust
use std::io::Read;
use flate2::read::GzDecoder;

fn gunzip_limited(data: &[u8], max: u64) -> std::io::Result<Vec<u8>> {
    let mut out = Vec::new();
    GzDecoder::new(data).take(max + 1).read_to_end(&mut out)?;
    if out.len() as u64 > max {
        return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "decompressed data too large"));
    }
    Ok(out)
}

take(max + 1) stops reading after one byte over the limit, so memory use is bounded by max.

4. Algorithmic cost

Hash tables keyed by attacker-controlled strings are vulnerable to collision floods if the hasher is predictable. The standard HashMap is safe by default; see faster hashers before changing it. The regex crate guarantees linear-time matching, so pattern backtracking is not a risk, but a huge pattern can use memory (size_limit). Sorting or quadratic loops over input lists need a cap on list length.

5. Time

rust
use std::time::Duration;
use tower_http::timeout::TimeoutLayer;

A timeout layer ends slow requests, which counters slow-body ("slowloris") attacks. Apply timeouts at every outbound call too (timeouts in Tokio).

6. Concurrency

Without a limit on in-flight work, N clients cause N simultaneous expensive operations. A tokio::sync::Semaphore or Tower's ConcurrencyLimitLayer bounds it, and a bounded queue pushes back instead of buffering forever.

A budget per request

Decide for each endpoint: max body, max depth, max items, max time, max concurrency. Write them in the handler's documentation. Fuzz the parser with cargo fuzz to find inputs that blow a budget.

Limits

Limits protect one process, and a determined distributed attack needs rate limiting and infrastructure defenses. The checks above do not replace authentication in front of expensive endpoints. The tower-http crate documents its layers at docs.rs/tower-http.

Taggeddos
Share

Keep reading

Flat illustration for the post “Request Limits and Slow Client Attacks in Rust Services”, composed around web.
Security

Request Limits and Slow Client Attacks in Rust Services

Slow clients and oversized requests exhaust connections and memory. Set header and body timeouts, body size limits, concurrency limits and keep-alive bounds at the proxy and in the Axum service with Tower layers.

5 min read

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.