Skip to content

Commit cdb5ee8

Browse files
Reject trailing bytes in WelcomeResponse and MetadataResponse bodies
Audit after the StmtResponse strict-decode tightening surfaced these two as the last remaining fixed-body response decoders without an explicit class-owned length check — both had been leaning on the underlying decode_uint64 helper to raise on short input, but silently accepted oversized bodies. Match the sibling pattern (DbResponse, EmptyResponse, ResultResponse, FilesResponse, StmtResponse): body must be exactly the declared size, with an error message naming the owning class so operators reading logs can trace the frame. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 93382b2 commit cdb5ee8

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ def encode_body(self) -> bytes:
191191

192192
@classmethod
193193
def decode_body(cls, data: bytes, schema: int = 0) -> "WelcomeResponse":
194+
if len(data) != 8:
195+
raise DecodeError(f"WelcomeResponse body must be exactly 8 bytes, got {len(data)}")
194196
heartbeat_timeout = decode_uint64(data)
195197
return cls(heartbeat_timeout)
196198

@@ -757,6 +759,8 @@ def encode_body(self) -> bytes:
757759

758760
@classmethod
759761
def decode_body(cls, data: bytes, schema: int = 0) -> "MetadataResponse":
762+
if len(data) != 16:
763+
raise DecodeError(f"MetadataResponse body must be exactly 16 bytes, got {len(data)}")
760764
failure_domain = decode_uint64(data)
761765
weight = decode_uint64(data[8:])
762766
return cls(failure_domain, weight)

tests/test_responses_strict_length.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,49 @@ def test_trailing_bytes_rejected_schema1(self) -> None:
136136
body = self._body_schema1() + b"\x02"
137137
with pytest.raises(DecodeError, match=r"StmtResponse schema=1 body must be exactly 24"):
138138
StmtResponse.decode_body(body, schema=1)
139+
140+
141+
class TestWelcomeResponseStrictLength:
142+
"""Body is uint64(heartbeat_timeout) — exactly 8 bytes."""
143+
144+
def test_short_body_rejected(self) -> None:
145+
from dqlitewire.messages.responses import WelcomeResponse
146+
147+
with pytest.raises(DecodeError, match=r"WelcomeResponse body must be exactly 8"):
148+
WelcomeResponse.decode_body(b"\x00" * 7)
149+
150+
def test_exact_length_accepted(self) -> None:
151+
from dqlitewire.messages.responses import WelcomeResponse
152+
153+
msg = WelcomeResponse.decode_body((42).to_bytes(8, "little"))
154+
assert msg.heartbeat_timeout == 42
155+
156+
def test_trailing_bytes_rejected(self) -> None:
157+
from dqlitewire.messages.responses import WelcomeResponse
158+
159+
with pytest.raises(DecodeError, match=r"WelcomeResponse body must be exactly 8"):
160+
WelcomeResponse.decode_body(b"\x00" * 9)
161+
162+
163+
class TestMetadataResponseStrictLength:
164+
"""Body is two uint64s (failure_domain + weight) — exactly 16 bytes."""
165+
166+
def test_short_body_rejected(self) -> None:
167+
from dqlitewire.messages.responses import MetadataResponse
168+
169+
with pytest.raises(DecodeError, match=r"MetadataResponse body must be exactly 16"):
170+
MetadataResponse.decode_body(b"\x00" * 15)
171+
172+
def test_exact_length_accepted(self) -> None:
173+
from dqlitewire.messages.responses import MetadataResponse
174+
175+
body = (3).to_bytes(8, "little") + (7).to_bytes(8, "little")
176+
msg = MetadataResponse.decode_body(body)
177+
assert msg.failure_domain == 3
178+
assert msg.weight == 7
179+
180+
def test_trailing_bytes_rejected(self) -> None:
181+
from dqlitewire.messages.responses import MetadataResponse
182+
183+
with pytest.raises(DecodeError, match=r"MetadataResponse body must be exactly 16"):
184+
MetadataResponse.decode_body(b"\x00" * 17)

0 commit comments

Comments
 (0)