Skip to content

Commit b38de06

Browse files
Cap server-declared num_params in StmtResponse decode
Every other count-bearing field decoded from a response already enforces an upper bound (_MAX_COLUMN_COUNT, _MAX_FILE_COUNT, _MAX_NODE_COUNT in responses.py, _MAX_PARAM_COUNT in tuples.py). StmtResponse.num_params was the outlier, accepting any uint64 the server put on the wire. Reuse the encoder cap from tuples.py so the client and server-side declarations stay in lockstep. A server declaring more parameters than a well-formed prepared statement could ever bind is either malicious or corrupt; surface as a clean DecodeError at the wire boundary instead of passing an unchecked value upward. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e522b05 commit b38de06

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from dqlitewire.exceptions import DecodeError, EncodeError
1616
from dqlitewire.messages.base import Message
1717
from dqlitewire.tuples import (
18+
_MAX_PARAM_COUNT,
1819
_ROW_DONE_MARKER,
1920
_ROW_PART_MARKER,
2021
RowMarker,
@@ -267,6 +268,15 @@ def decode_body(cls, data: bytes, schema: int = 0) -> "StmtResponse":
267268
db_id = decode_uint32(data)
268269
stmt_id = decode_uint32(data[4:])
269270
num_params = decode_uint64(data[8:])
271+
# Parity with the encoder cap in tuples.py: a server declaring a
272+
# prepared-statement parameter count above _MAX_PARAM_COUNT is
273+
# either malicious or corrupt. Other count-bearing decode paths
274+
# (_MAX_COLUMN_COUNT, _MAX_FILE_COUNT, _MAX_NODE_COUNT) already
275+
# enforce their own caps; this closes the matching gap.
276+
if num_params > _MAX_PARAM_COUNT:
277+
raise DecodeError(
278+
f"StmtResponse num_params {num_params} exceeds maximum ({_MAX_PARAM_COUNT})"
279+
)
270280
tail_offset = decode_uint64(data[16:]) if schema >= 1 else None
271281
return cls(db_id, stmt_id, num_params, tail_offset)
272282

tests/test_messages_responses.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,23 @@ def test_schema_1_rejects_v0_length_body(self) -> None:
221221
with pytest.raises(DecodeError, match="requires at least 24 bytes"):
222222
StmtResponse.decode_body(v0_body, schema=1)
223223

224+
def test_rejects_oversized_num_params(self) -> None:
225+
"""Defense-in-depth: cap server-declared num_params to match the
226+
encoder-side _MAX_PARAM_COUNT. A malicious or corrupt server
227+
returning num_params=2**63-1 produces a clean DecodeError, not
228+
an unchecked value that a cautious caller could trust.
229+
"""
230+
# Build a schema=0 body with num_params = 2**63 - 1.
231+
import struct
232+
233+
body = (
234+
struct.pack("<I", 1) # db_id
235+
+ struct.pack("<I", 2) # stmt_id
236+
+ struct.pack("<Q", 2**63 - 1) # num_params (bogus)
237+
)
238+
with pytest.raises(DecodeError, match="num_params"):
239+
StmtResponse.decode_body(body, schema=0)
240+
224241

225242
class TestResultResponse:
226243
def test_roundtrip(self) -> None:

0 commit comments

Comments
 (0)