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

The HTTP/1 content-length parser would accept lengths that were prefixed
with a plus sign (for example, `+1234`). The specification restricts the
content-length header to only allow DIGITs, making such a content-length
illegal. Since some HTTP implementations protect against that, and
others mis-interpret the length when the plus sign is present, this
fixes hyper to always reject such content lengths.

See GHSA-f3pg-qwvg-p99c
This commit is contained in:
Sean McArthur
2021-07-01 12:36:41 -07:00
parent 1068b994df
commit 1fb719e0b6
3 changed files with 87 additions and 6 deletions

View File

@@ -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 {

View File

@@ -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(