Nine security fixes in one release is not a record to be proud of — it’s a record of what a real audit finds when it stops assuming the happy path.
Today we released OxiHTTP 0.2.1 — a security-hardening release for the COOLJAPAN Pure-Rust HTTP stack. It closes a redirect credential leak, two distinct WebSocket unbounded-memory denial-of-service paths, a chunked-encoding body-limit bypass, a spoofable rate-limiter key, a client-side decompression bomb, a ServeDir symlink escape, a multipart header-injection bug, and a CORS caching-correctness gap — nine fixes, all landing in one coordinated pass.
No curl. No OpenSSL. No native-tls. No FFI, no -sys crates on the path you actually ship. OxiHTTP compiles to a single static binary with transport security handled entirely by OxiTLS — Pure Rust from the byte that hits the socket to the header that describes it.
Why OxiHTTP 0.2.1 is a game changer
A security-hardening pass earns its name by closing gaps that are easy to miss until someone goes looking for them:
- The redirect-following loop re-sent
AuthorizationandCookieheaders to a redirect target on a different host or scheme — a classic cross-origin credential leak whenever a client followed a 3xx it didn’t fully control. - Fragmented WebSocket reassembly, and separately a single unfragmented large frame, could grow
oxihttp-server’s internal buffer without limit — two independent routes to the same unbounded-memory DoS. - The request body size limit only checked the declared
Content-Lengthheader, so aTransfer-Encoding: chunkedrequest walked straight pastBodyLimit. - The token-bucket rate limiter keyed exclusively on the client-controlled
X-Forwarded-Forheader, defaulting every client without a reverse proxy to a shared"unknown"bucket — and never evicted idle buckets. Response::body_bytes()collected the wire body with no size cap and fed it to a one-shot decompressor with no output cap — a small, highly-compressible payload could force unbounded client-side allocation.ServeDir’s traversal check never resolved symlinks, so a symlink placed inside a served root pointing outside it was silently followed.- Multipart
Content-Disposition/Content-Typeparameters interpolated caller-suppliedname/filename/content_typewith no escaping — a stray"or CR/LF could inject a forged header or part boundary.
OxiHTTP 0.2.1 ends all of that.
- Redirect credential leak — fixed at the root. A new
same_origin()check (scheme + host, case-insensitive) now gates whetherAuthorization/Cookiesurvive a redirect hop; cookies re-added from the jar remain correctly scoped per-target-URL regardless. - Both WebSocket DoS paths closed by one budget.
WebSocket::set_max_message_size(default 16 MiB) now bounds fragmented reassembly and a single oversized frame — the wire-level per-frame cap is derived from the same budget, floored at 125 bytes so RFC 6455 §5.5 control frames (Ping/Pong/Close) stay deliverable even under a tiny configured limit. - Chunked requests now hit the same
BodyLimit.MiddlewarePipeline::inject_body_limitinjects the limit on every accept path, andRequest::body_bytesenforces it against the actually-decoded size — not the declared one. - The rate limiter now keys on the real TCP peer by default, with
X-Forwarded-Foronly honored when a trusted reverse proxy is explicitly configured viawith_trusted_proxy_headers. Idle buckets are swept every 5 minutes and total buckets are hard-capped at 100,000. - Client response collection is bounded.
ClientBuilder::with_max_response_body(default 64 MiB, per-request overridable) caps wire-body collection, and gzip/zlib decoding now uses a bounded, CRC-32-verified streaming decoder instead of a one-shot unbounded API. ServeDir::with_symlink_protection(true)re-validates the canonicalized path against the served root and returns403 Forbiddenon an escape — opt-in, for operators serving directories untrusted users can write into.- Multipart headers are now escaped.
"and\are backslash-escaped per RFC 9110 §5.6.4, and CR/LF are stripped outright — there is no valid escape for a raw control character in a header value. - 320 tests passing with default features (446 with
--all-features, plus 56 doctests), zero clippy/compiler/rustdoc warnings.
Technical Deep Dive: where the fixes live
- The client (
oxihttp-client). The redirect same-origin check, the response-body size cap, and the bounded streaming decompressor all live here — together they close both the credential-leak and decompression-bomb classes of client-side bugs. - The server (
oxihttp-server). The WebSocket message-size budget (ws.rs/ws_frame.rs), the chunked-encodingBodyLimitfix, the rate limiter’s peer-address default and bucket eviction,ServeDir’s streaming file bodies and symlink protection, and the CORSVarycorrectness fix all landed in this crate. - The core types (
oxihttp-core). Multipart header escaping forPart::text/Part::file/add_file_stream—Part::customremains the documented, unsanitized escape hatch for callers who need fully custom headers. - New fuzz coverage (
fuzz/, unpublished workspace).ws_frame_read,cookie_parse,range_header, andmultipart_build— coverage-guidedcargo-fuzztargets that exercise exactly the parsers this release hardened.
Getting Started
cargo add oxihttp
The client and server API is unchanged in 0.2.1 — every fix in this release is a security correction, not a surface change:
use oxihttp::prelude::*;
// Client: response body size is now capped by default (64 MiB)
let client = Client::builder()
.with_tls()
.with_retry(RetryPolicy::default())
.build()?;
let body: serde_json::Value = client
.get("https://httpbin.org/json")
.send()
.await?
.json()
.await?;
use oxihttp::prelude::*;
// Server: WebSocket reassembly and single-frame size are now bounded
let router = Router::new().get("/ws", |req| async move {
let mut ws = req.upgrade_websocket().await?;
ws.set_max_message_size(4 * 1024 * 1024); // 4 MiB, tighter than the 16 MiB default
Ok(ws)
});
Server::builder().bind("0.0.0.0:8080").serve(router).await?;
What’s New in 0.2.1
- Fixed (Security): redirect credential leak across origins; WebSocket unbounded-memory DoS via reassembly and via a single unfragmented frame; unmasked-client-frame acceptance (RFC 6455 §5.1 conformance); chunked-encoding
BodyLimitbypass; spoofable rate-limiter key plus unbounded bucket growth; client-side response/decompression bomb; silent compressed-bytes-as-plaintext bug whendecompressionis compiled off;ServeDir/ServeFileper-request memory blowup on large files; CORSVarycaching-correctness gap;ServeDirsymlink escapes (opt-in fix); multipart header injection via unescapedname/filename/content_type. - Added:
WebSocket::set_max_message_size,ClientBuilder::with_max_response_body/RequestBuilder::max_response_body,RateLimiter::with_limits/bucket_count,MiddlewarePipeline::with_trusted_proxy_headers,MultipartBuilder::add_stream_part/add_file_stream(zero-copy streaming multipart),RequestBuilder::multipart_stream,ServeDir::with_symlink_protection, plusSECURITY.md/CONTRIBUTING.mdand coverage-guided fuzz targets. - Changed: sibling COOLJAPAN dependencies bumped —
oxitls0.2.0 → 0.3.0 (clearing RUSTSEC-2026-0104’s CRL-parsing panic exposure),oxiarc-deflate/oxiarc-core→ 0.4.1,oxiquic-h3/oxiquic-crypto→ 0.2.1;ServeDirETags switched from a content hash to file metadata (mtime + length), enabling the new streaming file-body implementation. - Fixed (non-security): three private intra-doc rustdoc links reworded to plain code spans.
Tips
- If you serve files, opt into symlink protection when the root is writable by untrusted users.
ServeDir::with_symlink_protection(true)is opt-in (matching nginx/tower-http’s default lexical-only behavior) because canonicalizing every path has a cost — turn it on specifically when that tradeoff is worth it. - If you’re behind a reverse proxy, call
with_trusted_proxy_headers(true)explicitly. The rate limiter no longer trustsX-Forwarded-Forby default — without this call, every request is now correctly keyed on the actual TCP peer, which is wrong if you’re actually behind a proxy and want the real client IP. - Tune
WebSocket::set_max_message_sizeto your workload, not just the 16 MiB default — the same budget now also caps single unfragmented frames, so a too-low value can reject a legitimate large message rather than just a malicious one. - If you build multipart bodies from user-supplied filenames, you no longer need to sanitize
"/CR/LF yourself before callingPart::file/Part::text— it’s handled internally now.Part::customis still there if you need to bypass this. - Large file transfers now stream by default —
ServeDir/ServeFileno longer buffer the whole file (or byte range) into memory, so multi-gigabyte files and many concurrent range requests are no longer a per-request memory hazard. - Coming from a version before the
oxitls0.3.0 bump? If your dependency tree still resolvesoxitls 0.2.0anywhere, it carries the RUSTSEC-2026-0104 exposure this release’s transitive bump clears —cargo update -p oxitlsafter upgrading.
This is the foundation
OxiHTTP is part of NoFFI — the COOLJAPAN initiative to replace every C/C++/Fortran/-sys FFI dependency in the Rust world with a clean, memory-safe, 100% Pure Rust implementation. A security-hardening release that closes nine real gaps in one pass is exactly the kind of work that keeps that promise credible for production traffic, not just for a demo.
It stands shoulder to shoulder with its siblings: OxiTLS for transport security, OxiQUIC for QUIC/HTTP-3 transport, and OxiARC for compression — with OxiStore, OxiRPC, and a growing list of COOLJAPAN projects carrying it as their HTTP client layer.
Repository: https://github.com/cool-japan/oxihttp
Star the repo if a Pure-Rust HTTP stack that treats a redirect credential leak and a WebSocket DoS as equally unacceptable is something you’ve been waiting for.
The era of shipping HTTP security fixes one CVE report at a time is over. Pure Rust HTTP — audited, hardened, and sovereign — is here.
— KitaSan at COOLJAPAN OÜ August 7, 2026