Merge commit from fork

This commit is contained in:
Yuki Okushi 2026-04-18 11:09:12 +09:00 committed by GitHub
parent e6d09913d9
commit 3c056bd361
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 83 additions and 10 deletions

View File

@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Reject HTTP/1 requests with ambiguous request framing from `Content-Length` and `Transfer-Encoding` headers to prevent request smuggling.
- Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953] - Encode the HTTP/1 `Connection: Upgrade` header in Camel-Case when camel-case header formatting is enabled.[#3953]
- Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers. - Fix `HeaderMap` iterators' `len()` and `size_hint()` implementations for multi-value headers.
- Update `rand` dependency to `0.10`. - Update `rand` dependency to `0.10`.

View File

@ -237,4 +237,18 @@ mod tests {
assert_eq!(*req.method(), Method::POST); assert_eq!(*req.method(), Method::POST);
assert!(req.chunked().unwrap()); assert!(req.chunked().unwrap());
} }
#[actix_rt::test]
async fn test_http_request_rejects_content_length_and_chunked() {
let mut codec = Codec::default();
let mut buf = BytesMut::from(
"POST /test HTTP/1.1\r\n\
content-length: 11\r\n\
transfer-encoding: chunked\r\n\r\n\
0\r\n\r\n\
GET /test2 HTTP/1.1\r\n\r\n",
);
assert!(codec.decode(&mut buf).is_err());
}
} }

View File

@ -275,6 +275,23 @@ impl MessageType for Request {
// convert headers // convert headers
let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
if msg.head().headers.contains_key(header::TRANSFER_ENCODING) {
if ver == Version::HTTP_10 {
debug!("Transfer-Encoding is not allowed in HTTP/1.0 requests");
return Err(ParseError::Header);
}
if !crate::HttpMessage::chunked(&msg)? {
debug!("request Transfer-Encoding must be chunked");
return Err(ParseError::Header);
}
if msg.head().headers.contains_key(header::CONTENT_LENGTH) {
debug!("both Content-Length and Transfer-Encoding are set");
return Err(ParseError::Header);
}
}
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers // disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2 // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() { if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
@ -1116,18 +1133,57 @@ mod tests {
#[test] #[test]
fn hrs_cl_and_te_http10() { fn hrs_cl_and_te_http10() {
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both expect_parse_err!(&mut BytesMut::from(
let mut buf = BytesMut::from(
"GET / HTTP/1.0\r\n\ "GET / HTTP/1.0\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: 3\r\n\ Content-Length: 3\r\n\
Transfer-Encoding: chunked\r\n\ Transfer-Encoding: chunked\r\n\
\r\n\ \r\n\
000", 000",
); ));
}
parse_ready!(&mut buf); #[test]
fn hrs_cl_and_chunked_te_http11() {
expect_parse_err!(&mut BytesMut::from(
"POST / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 3\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
0\r\n\
\r\n",
));
expect_parse_err!(&mut BytesMut::from(
"POST / HTTP/1.1\r\n\
Host: example.com\r\n\
Transfer-Encoding: chunked\r\n\
Content-Length: 3\r\n\
\r\n\
0\r\n\
\r\n",
));
}
#[test]
fn hrs_identity_te_http11() {
expect_parse_err!(&mut BytesMut::from(
"POST / HTTP/1.1\r\n\
Host: example.com\r\n\
Transfer-Encoding: identity\r\n\
\r\n\
0\r\n",
));
expect_parse_err!(&mut BytesMut::from(
"POST / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 3\r\n\
Transfer-Encoding: identity\r\n\
\r\n\
0\r\n",
));
} }
#[test] #[test]
@ -1165,14 +1221,16 @@ mod tests {
} }
#[test] #[test]
fn transfer_encoding_agrees() { fn hrs_chunked_te_http11() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
Host: example.com\r\n\ Host: example.com\r\n\
Content-Length: 3\r\n\ Transfer-Encoding: chunked\r\n\
Transfer-Encoding: identity\r\n\
\r\n\ \r\n\
0\r\n", 1\r\n\
a\r\n\
0\r\n\
\r\n",
); );
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
@ -1180,6 +1238,6 @@ mod tests {
let mut pl = pl.unwrap(); let mut pl = pl.unwrap();
let chunk = pl.decode(&mut buf).unwrap().unwrap(); let chunk = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n"))); assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"a")));
} }
} }