Lenient Parsing of Content-Length Header When Prefixed with Plus Sign
Description
hyper is an HTTP library for rust. hyper's HTTP/1 server code had a flaw that incorrectly parses and accepts requests with a Content-Length header with a prefixed plus sign, when it should have been rejected as illegal. This combined with an upstream HTTP proxy that doesn't parse such Content-Length headers, but forwards them, can result in "request smuggling" or "desync attacks". The flaw exists in all prior versions of hyper prior to 0.14.10, if built with rustc v1.5.0 or newer. The vulnerability is patched in hyper version 0.14.10. Two workarounds exist: One may reject requests manually that contain a plus sign prefix in the Content-Length header or ensure any upstream proxy handles Content-Length headers with a plus sign prefix.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
hyper HTTP/1 server incorrectly parses Content-Length headers with a plus sign prefix, enabling request smuggling via upstream proxies.
Vulnerability
hyper, an HTTP library for Rust, contains a flaw in its HTTP/1 server code that incorrectly parses and accepts requests with a Content-Length header containing a prefixed plus sign (e.g., +5), which should be rejected as illegal per RFC specifications [1]. This issue affects all versions of hyper prior to 0.14.10 when built with rustc v1.5.0 or newer [1]. The root cause traces back to a code change in Rust's integer parsing that allowed leading plus signs, which hyper's parser inadvertently adopted [2]. The fix in 0.14.10 introduces a custom from_digits() function that rejects non-digit characters such as the plus sign [3].
Exploitation
An attacker can send a crafted HTTP/1 request with a Content-Length header like +5 to a vulnerable hyper server. If an upstream HTTP proxy does not parse such malformed headers and simply forwards them, the hyper server may interpret the Content-Length differently than the proxy, leading to request smuggling or desync attacks [1]. No special privileges are required; the attack is conducted over the network with low complexity [4].
Impact
Successful exploitation allows an attacker to smuggle requests, potentially bypassing security controls, accessing unauthorized resources, or poisoning caches. The integrity impact is low, as the attacker can manipulate request boundaries, but confidentiality and availability are not directly affected [4]. The attack scope remains unchanged.
Mitigation
The vulnerability is patched in hyper version 0.14.10 [1]. Users should upgrade to this version or later. Two workarounds exist: manually reject incoming requests that contain a plus sign prefix in the Content-Length header, or ensure that any upstream proxy correctly parses and rejects such malformed headers [1]. The vulnerability is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog.
- NVD - CVE-2021-32715
- Integer parsing should accept leading plus by arthurprs · Pull Request #28826 · rust-lang/rust
- fix(http1): reject content-lengths that have a plus sign prefix · hyperium/hyper@1fb719e
- Lenient `hyper` header parsing of `Content-Length` could allow request smuggling › RustSec Advisory Database
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
hypercrates.io | < 0.14.10 | 0.14.10 |
Affected products
3- osv-coords2 versions
< 0.5.4-r2+ 1 more
- (no CPE)range: < 0.5.4-r2
- (no CPE)range: < 0.14.10
- hyperium/hyperv5Range: < 0.14.10
Patches
11fb719e0b61afix(http1): reject content-lengths that have a plus sign prefix
3 files changed · +87 −6
src/headers.rs+29 −2 modified@@ -30,7 +30,7 @@ fn connection_has(value: &HeaderValue, needle: &str) -> bool { #[cfg(all(feature = "http1", feature = "server"))] pub(super) fn content_length_parse(value: &HeaderValue) -> Option<u64> { - value.to_str().ok().and_then(|s| s.parse().ok()) + from_digits(value.as_bytes()) } pub(super) fn content_length_parse_all(headers: &HeaderMap) -> Option<u64> { @@ -46,7 +46,7 @@ pub(super) fn content_length_parse_all_values(values: ValueIter<'_, HeaderValue> for h in values { if let Ok(line) = h.to_str() { for v in line.split(',') { - if let Some(n) = v.trim().parse().ok() { + if let Some(n) = from_digits(v.trim().as_bytes()) { if content_length.is_none() { content_length = Some(n) } else if content_length != Some(n) { @@ -64,6 +64,33 @@ pub(super) fn content_length_parse_all_values(values: ValueIter<'_, HeaderValue> return content_length } +fn from_digits(bytes: &[u8]) -> Option<u64> { + // cannot use FromStr for u64, since it allows a signed prefix + let mut result = 0u64; + const RADIX: u64 = 10; + + if bytes.is_empty() { + return None; + } + + for &b in bytes { + // can't use char::to_digit, since we haven't verified these bytes + // are utf-8. + match b { + b'0'..=b'9' => { + result = result.checked_mul(RADIX)?; + result = result.checked_add((b - b'0') as u64)?; + }, + _ => { + // not a DIGIT, get outta here! + return None; + } + } + } + + Some(result) +} + #[cfg(all(feature = "http2", feature = "client"))] pub(super) fn method_has_defined_payload_semantics(method: &Method) -> bool { match *method {
src/proto/h1/role.rs+20 −4 modified@@ -219,10 +219,8 @@ impl Http1Transaction for Server { if is_te { continue; } - let len = value - .to_str() - .map_err(|_| Parse::content_length_invalid()) - .and_then(|s| s.parse().map_err(|_| Parse::content_length_invalid()))?; + let len = headers::content_length_parse(&value) + .ok_or_else(Parse::content_length_invalid)?; if let Some(prev) = con_len { if prev != len { debug!( @@ -1775,6 +1773,16 @@ mod tests { "multiple content-lengths", ); + // content-length with prefix is not allowed + parse_err( + "\ + POST / HTTP/1.1\r\n\ + content-length: +10\r\n\ + \r\n\ + ", + "prefixed content-length", + ); + // transfer-encoding that isn't chunked is an error parse_err( "\ @@ -1958,6 +1966,14 @@ mod tests { ", ); + parse_err( + "\ + HTTP/1.1 200 OK\r\n\ + content-length: +8\r\n\ + \r\n\ + ", + ); + // transfer-encoding: chunked assert_eq!( parse(
tests/server.rs+38 −0 modified@@ -405,6 +405,44 @@ fn get_chunked_response_with_ka() { read_until(&mut req, |buf| buf.ends_with(quux)).expect("reading 2"); } +#[test] +fn post_with_content_length_body() { + let server = serve(); + let mut req = connect(server.addr()); + req.write_all( + b"\ + POST / HTTP/1.1\r\n\ + Content-Length: 5\r\n\ + \r\n\ + hello\ + ", + ) + .unwrap(); + req.read(&mut [0; 256]).unwrap(); + + assert_eq!(server.body(), b"hello"); +} + +#[test] +fn post_with_invalid_prefix_content_length() { + let server = serve(); + let mut req = connect(server.addr()); + req.write_all( + b"\ + POST / HTTP/1.1\r\n\ + Content-Length: +5\r\n\ + \r\n\ + hello\ + ", + ) + .unwrap(); + + let mut buf = [0; 256]; + let _n = req.read(&mut buf).unwrap(); + let expected = "HTTP/1.1 400 Bad Request\r\n"; + assert_eq!(s(&buf[..expected.len()]), expected); +} + #[test] fn post_with_chunked_body() { let server = serve();
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-f3pg-qwvg-p99cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32715ghsaADVISORY
- github.com/hyperium/hyper/commit/1fb719e0b61a4f3d911562a436a2ff05fd7cb759ghsaWEB
- github.com/hyperium/hyper/security/advisories/GHSA-f3pg-qwvg-p99cghsax_refsource_CONFIRMWEB
- github.com/rust-lang/rust/pull/28826/commits/123a83326fb95366e94a3be1a74775df4db97739ghsax_refsource_MISCWEB
- rustsec.org/advisories/RUSTSEC-2021-0078.htmlghsaWEB
News mentions
0No linked articles in our index yet.