VYPR
Low severityNVD Advisory· Published Jul 7, 2021· Updated Aug 3, 2024

Lenient Parsing of Content-Length Header When Prefixed with Plus Sign

CVE-2021-32715

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.

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.

PackageAffected versionsPatched versions
hypercrates.io
< 0.14.100.14.10

Affected products

3

Patches

1
1fb719e0b61a

fix(http1): reject content-lengths that have a plus sign prefix

https://github.com/hyperium/hyperSean McArthurJul 1, 2021via ghsa
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

News mentions

0

No linked articles in our index yet.