diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 750102f..b56125a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,7 +19,8 @@ dev **Bugfixes** -- +- Preserve the full padded HEADERS body length and reject padding that overlaps + fixed fields in HEADERS and PUSH_PROMISE frames. 6.1.0 (2025-01-22) ------------------ diff --git a/src/hyperframe/frame.py b/src/hyperframe/frame.py index 5bab673..a88361d 100644 --- a/src/hyperframe/frame.py +++ b/src/hyperframe/frame.py @@ -538,7 +538,7 @@ def parse_body(self, data: memoryview) -> None: msg = f"Invalid PUSH_PROMISE promised stream id: {self.promised_stream_id}" raise InvalidDataError(msg) - if self.pad_length and self.pad_length >= self.body_len: + if self.pad_length > self.body_len - padding_data_length - 4: msg = "Padding is too long." raise InvalidPaddingError(msg) @@ -751,6 +751,7 @@ def serialize_body(self) -> bytes: return b"".join([padding_data, priority_data, self.data, padding]) def parse_body(self, data: memoryview) -> None: + self.body_len = len(data) padding_data_length = self.parse_padding_data(data) data = data[padding_data_length:] @@ -759,12 +760,11 @@ def parse_body(self, data: memoryview) -> None: else: priority_data_length = 0 - self.body_len = len(data) self.data = ( data[priority_data_length:len(data)-self.pad_length].tobytes() ) - if self.pad_length and self.pad_length >= self.body_len: + if self.pad_length > len(data) - priority_data_length: msg = "Padding is too long." raise InvalidPaddingError(msg) diff --git a/tests/test_frames.py b/tests/test_frames.py index b683354..7cd1d4e 100644 --- a/tests/test_frames.py +++ b/tests/test_frames.py @@ -995,3 +995,32 @@ class TestExtensionFrame: def test_repr(self): f = ExtensionFrame(0xFF, 1, 42, b'hello') assert repr(f).endswith("type=255, flag_byte=42, body=") + + +@pytest.mark.parametrize('pad_length', [0, 1, 5]) +@pytest.mark.parametrize('payload', [b'', b'hello']) +@pytest.mark.parametrize('priority', [False, True]) +def test_padded_headers_body_length(pad_length, payload, priority): + flags = ['PADDED', 'END_HEADERS'] + if priority: + flags.append('PRIORITY') + frame = HeadersFrame(1, flags=flags, pad_length=pad_length, data=payload) + wire = frame.serialize() + decoded = decode_frame(wire) + assert decoded.data == payload + assert decoded.body_len == len(wire) - 9 + assert decoded.serialize() == wire + + +@pytest.mark.parametrize('frame_type', [HeadersFrame, PushPromiseFrame]) +def test_padding_cannot_overlap_fixed_fields(frame_type): + # The pad-length byte claims one padding byte, but all remaining bytes + # belong to the mandatory priority or promised-stream-ID field. + if frame_type is HeadersFrame: + frame = HeadersFrame(1, flags=['PADDED', 'PRIORITY']) + body = b'\x01\x00\x00\x00\x00\x00' + else: + frame = PushPromiseFrame(1, flags=['PADDED']) + body = b'\x01\x00\x00\x00\x02' + with pytest.raises(InvalidPaddingError): + frame.parse_body(memoryview(body))