cargo / axum / audit
cargo : axum @ 0.8.9
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

concurrency-documentedconcurrency-safecrypto-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-correctparser-impl-safeparser-impl-testedprotocol-impl-correctprotocol-impl-safeprotocol-impl-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

axum 0.8.9 is an HTTP routing and request-handling library built on tokio, hyper, and tower. The crate enforces uses-unsafe via a crate-wide forbid directive. All body-consuming extractors apply a configurable 2 MB default body limit (parser-impl-safe, protocol-impl-safe). No findings were recorded.

Report

Subject

axum 0.8.9 is an HTTP routing and request-handling library for Rust built on tokio, hyper, and the tower middleware ecosystem. It exposes a macro-free Router API backed by a radix-tree router (matchit), a typed extractor system (FromRequest/FromRequestParts), and response building via IntoResponse. Its distinguishing design choice is that it does not provide its own middleware system; instead it exposes tower Service directly, letting users compose axum applications with any tower or tower-http middleware.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. The comparison revealed only the expected normalised Cargo.toml difference; all source files matched byte-for-byte. The entire src/ tree (~19 400 lines across 50 files) was surveyed. Files central to the audit brief were read in full: src/extract/multipart.rs, src/extract/ws.rs, src/json.rs, src/form.rs, src/routing/path_router.rs, src/routing/mod.rs, src/serve/mod.rs, src/extract/path/mod.rs, and src/lib.rs. The remaining files were surveyed for unsafe, FFI, network calls, filesystem access, and process execution using grep. The test suite was not run, but inline unit tests, integration tests in src/routing/tests/, and property tests in src/routing/strip_prefix.rs were read. Tools used: openvet 0.6.0, diff, grep/ripgrep.

Results

The VCS diff is clean. The crate ships no binary artefacts and no build.rs, justifying has-binaries and has-build-exec. The [lints.rust] unsafe_code = "forbid" directive in the manifest, confirmed by a source-wide grep returning no unsafe tokens, justifies uses-unsafe. No filesystem, process-execution, or environment-variable access was found in any source file, justifying uses-filesystem, uses-exec, and uses-environment. The crate does not open network connections itself; it accepts an already-bound listener from the caller, justifying uses-network.

The crate uses SHA-1 and base64 (via the sha1 and base64 crates) in src/extract/ws.rs to compute the Sec-WebSocket-Accept handshake header mandated by RFC 6455. This is the only cryptographic operation. The operation is protocol-mandated and not security-sensitive; justifies uses-crypto and crypto-safe.

The Router<S> inner state is wrapped in Arc for cheap per-request clones, and the serve module uses tokio::sync::watch for graceful shutdown. Both patterns are standard tokio usage with no shared mutable state; justifies uses-concurrency, concurrency-safe, and concurrency-documented.

axum implements parsers for URL path parameters (custom serde::Deserializer in src/extract/path/de.rs), multipart bodies (delegating parsing to multer, wrapping the stream after applying the body limit), URL-encoded forms (delegating to serde_urlencoded), and JSON (delegating to serde_json). It also implements the WebSocket upgrade handshake protocol. These justify impl-parser and impl-protocol. All body-consuming extractors call req.with_limited_body() or route through Bytes::from_request, both of which enforce the DefaultBodyLimit (2 MB by default, configurable with DefaultBodyLimit::max or disableable with DefaultBodyLimit::disable). Oversized requests are rejected with 413. This validates the key DoS surface raised in the audit brief; parser-impl-safe holds. The body_limited_by_default integration test in src/routing/tests/mod.rs and the body_too_large test in src/extract/multipart.rs cover the limit paths; parser-impl-tested and protocol-impl-tested hold.

The crate contains inline unit tests, integration tests under src/routing/tests/, and quickcheck property tests in src/routing/strip_prefix.rs, justifying has-unit-tests, has-integration-tests, and has-property-tests.

The codebase was reviewed for JIT compilation, embedded interpreters, custom cryptographic implementations, custom concurrency primitives, and custom data structures. None were found; justifying uses-jit, uses-interpreter, impl-jit, impl-interpreter, impl-crypto, impl-concurrency, and impl-datastructure. No custom sorting, searching, or compression algorithms are implemented, justifying impl-algorithm. No install-time execution hooks exist, justifying has-install-exec. No fuzz tests were found in the published package or the VCS checkout, justifying has-fuzz-tests. Conformance to the WebSocket RFC and path parser specification was not checked against reference test vectors, so parser-impl-correct and protocol-impl-correct are not asserted.

No malicious code was found. The source is straightforward HTTP glue with no obfuscation, no telemetry, and no external network calls. Justifies is-benign.

No findings were recorded.

Conclusion

The crate is a large but well-structured HTTP routing framework. The #![forbid(unsafe_code)] declaration is enforced and confirmed. All body-consuming extractors apply the 2 MB default body limit before parsing, addressing the primary DoS surface for JSON, form, and multipart inputs. The WebSocket extractor delegates size limits to tokio-tungstenite's configurable message-size cap (defaulting to 64 MB per message). No unsafe code, no I/O, no process execution, and no environment access were found in the crate's own source. The test suite covers extractors, routing edge cases, body-limit enforcement, and WebSocket upgrade logic.

Findings

No findings.

Annotations(5)

Cargo.toml

The crate-level #![forbid(unsafe_code)] lint (declared via [lints.rust] unsafe_code = "forbid" in Cargo.toml.orig) ensures no unsafe blocks can appear anywhere in the crate. Confirmed with grep -rEn '\bunsafe\b' contents/src/ returning no results. Justifies uses-unsafe and has-build-exec (no build.rs present).

src/extract/multipart.rs

src/extract/multipart.rs, line 74-81

    async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
        let boundary = content_type_str(req.headers())
            .and_then(|content_type| multer::parse_boundary(content_type).ok())
            .ok_or(InvalidBoundary)?;
        let stream = req.with_limited_body().into_body();
        let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
        Ok(Self { inner: multipart })
    }

The Multipart extractor calls req.with_limited_body() before passing the stream to multer::Multipart::new, ensuring the default body limit (2 MB, defined in axum-core) is enforced before any multipart parsing occurs. Oversized requests are rejected with 413 Payload Too Large. Justifies parser-impl-safe and parser-impl-tested (the body_too_large test covers the limit enforcement path).

src/extract/ws.rs

The ws feature uses sha1 and base64 to compute the Sec-WebSocket-Accept header as required by RFC 6455. This is the only cryptographic operation in the crate. The key is hashed with SHA-1 and encoded with base64 — this is the protocol-mandated handshake, not a security-sensitive cryptographic primitive. Justifies uses-crypto and crypto-safe. The extractor validates the Upgrade, Connection, and Sec-WebSocket-Key headers and checks the HTTP version before accepting the upgrade. Justifies protocol-impl-safe and protocol-impl-tested (covered by integration tests in the test_helpers module).

src/json.rs

src/json.rs, line 106-113

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        if !json_content_type(req.headers()) {
            return Err(MissingJsonContentType.into());
        }

        let bytes = Bytes::from_request(req, state).await?;
        Self::from_bytes(&bytes)
    }

The Json extractor buffers the full body via Bytes::from_request, which applies the DefaultBodyLimit (2 MB default) before deserializing. Content-type is validated before buffering. Justifies parser-impl-safe. Inline tests cover missing content-type (415), trailing chars (400), type mismatch (422), and syntax error (400) cases. Justifies parser-impl-tested.

src/routing/mod.rs

src/routing/mod.rs, line 68-70

pub struct Router<S = ()> {
    inner: Arc<RouterInner<S>>,
}

The Router inner state is wrapped in Arc<RouterInner<S>> for cheap clones across async tasks. The serve module uses tokio::sync::watch for graceful shutdown signalling. Neither structure exposes shared mutable state; the router is immutable after construction and the watch channel follows its documented API. Justifies uses-concurrency, concurrency-safe, and concurrency-documented.