Skip to content

Commit 24a065f

Browse files
committed
Header frame with non-zero content-length and END_STREAM is malformed
Before this change, content-length underflow is only checked when receiving date frames. The underflow error was never triggered if data frames are never received. This change adds similar check for headers frames.
1 parent 87c9be0 commit 24a065f

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

src/frame/headers.rs

+4
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,10 @@ impl Headers {
254254
&mut self.header_block.pseudo
255255
}
256256

257+
pub(crate) fn pseudo(&self) -> &Pseudo {
258+
&self.header_block.pseudo
259+
}
260+
257261
/// Whether it has status 1xx
258262
pub(crate) fn is_informational(&self) -> bool {
259263
self.header_block.pseudo.is_informational()

src/proto/streams/recv.rs

+12
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,18 @@ impl Recv {
185185
};
186186

187187
stream.content_length = ContentLength::Remaining(content_length);
188+
// END_STREAM on headers frame with non-zero content-length is malformed.
189+
// https://datatracker.ietf.org/doc/html/rfc9113#section-8.1.1
190+
if frame.is_end_stream()
191+
&& content_length > 0
192+
&& frame
193+
.pseudo()
194+
.status
195+
.map_or(true, |status| status != 204 && status != 304)
196+
{
197+
proto_err!(stream: "recv_headers with END_STREAM: content-length is not zero; stream={:?};", stream.id);
198+
return Err(Error::library_reset(stream.id, Reason::PROTOCOL_ERROR).into());
199+
}
188200
}
189201
}
190202

tests/h2-tests/tests/client_request.rs

+43
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,49 @@ async fn allow_empty_data_for_head() {
13511351
join(srv, h2).await;
13521352
}
13531353

1354+
#[tokio::test]
1355+
async fn reject_none_zero_content_length_header_with_end_stream() {
1356+
h2_support::trace_init!();
1357+
let (io, mut srv) = mock::new();
1358+
1359+
let srv = async move {
1360+
let settings = srv.assert_client_handshake().await;
1361+
assert_default_settings!(settings);
1362+
srv.recv_frame(
1363+
frames::headers(1)
1364+
.request("GET", "https://example.com/")
1365+
.eos(),
1366+
)
1367+
.await;
1368+
srv.send_frame(
1369+
frames::headers(1)
1370+
.response(200)
1371+
.field("content-length", 100)
1372+
.eos(),
1373+
)
1374+
.await;
1375+
};
1376+
1377+
let h2 = async move {
1378+
let (mut client, h2) = client::Builder::new()
1379+
.handshake::<_, Bytes>(io)
1380+
.await
1381+
.unwrap();
1382+
tokio::spawn(async {
1383+
h2.await.expect("connection failed");
1384+
});
1385+
let request = Request::builder()
1386+
.method(Method::GET)
1387+
.uri("https://example.com/")
1388+
.body(())
1389+
.unwrap();
1390+
let (response, _) = client.send_request(request, true).unwrap();
1391+
let _ = response.await.unwrap_err();
1392+
};
1393+
1394+
join(srv, h2).await;
1395+
}
1396+
13541397
#[tokio::test]
13551398
async fn early_hints() {
13561399
h2_support::trace_init!();

0 commit comments

Comments
 (0)