Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
------------------
Expand Down
6 changes: 3 additions & 3 deletions src/hyperframe/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:]

Expand All @@ -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)

Expand Down
29 changes: 29 additions & 0 deletions tests/test_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=<hex:68656c6c6f>")


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