Skip to content

Commit 48f362e

Browse files
fix: validate schema version in decode_continuation
decode_continuation() now checks header.schema against _MAX_SCHEMA, matching the validation already done in decode_bytes(). Previously a corrupted or malicious continuation frame with an unsupported schema version would be silently accepted. Closes #099 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4204053 commit 48f362e

2 files changed

Lines changed: 44 additions & 0 deletions

File tree

src/dqlitewire/codec.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ def decode_continuation(self) -> RowsResponse | None:
274274
header = Header.decode(data[:HEADER_SIZE])
275275
body = data[HEADER_SIZE : HEADER_SIZE + header.size_words * 8]
276276

277+
max_schema = _MAX_SCHEMA.get(header.msg_type, 0)
278+
if header.schema > max_schema:
279+
raise DecodeError(
280+
f"Unsupported schema version {header.schema} for message type "
281+
f"{header.msg_type} (max supported: {max_schema})"
282+
)
283+
277284
if header.msg_type == ResponseType.FAILURE:
278285
failure = FailureResponse.decode_body(body, schema=header.schema)
279286
raise ProtocolError(

tests/test_codec.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,3 +1616,40 @@ def test_chained_continuations_part_part_done(self) -> None:
16161616
decoder.feed(normal.encode())
16171617
msg4 = decoder.decode()
16181618
assert isinstance(msg4, ResultResponse)
1619+
1620+
def test_decode_continuation_rejects_unsupported_schema(self) -> None:
1621+
"""099: decode_continuation must validate schema like decode_bytes."""
1622+
import struct
1623+
1624+
from dqlitewire.constants import ValueType
1625+
from dqlitewire.messages.responses import RowsResponse
1626+
1627+
# Build an initial ROWS frame with has_more=True
1628+
initial = RowsResponse(
1629+
column_names=["x"],
1630+
column_types=[ValueType.INTEGER],
1631+
rows=[[1]],
1632+
has_more=True,
1633+
)
1634+
# Build a continuation frame with schema=1 (unsupported for ROWS)
1635+
cont_body = RowsResponse(
1636+
column_names=["x"],
1637+
column_types=[ValueType.INTEGER],
1638+
rows=[[2]],
1639+
has_more=False,
1640+
).encode_body()
1641+
# Manually build header with schema=1
1642+
from dqlitewire.constants import ResponseType
1643+
1644+
size_words = len(cont_body) // 8
1645+
bad_header = struct.pack("<IBBH", size_words, ResponseType.ROWS, 1, 0)
1646+
bad_frame = bad_header + cont_body
1647+
1648+
decoder = MessageDecoder(is_request=False)
1649+
decoder.feed(initial.encode() + bad_frame)
1650+
1651+
msg1 = decoder.decode()
1652+
assert isinstance(msg1, RowsResponse) and msg1.has_more
1653+
1654+
with pytest.raises(DecodeError, match="Unsupported schema version"):
1655+
decoder.decode_continuation()

0 commit comments

Comments
 (0)