Skip to content

Commit 17cd924

Browse files
Cap column name length in RowsResponse decoder
A hostile or buggy server can pack a giant column name inside a frame-legal RowsResponse and force unbounded Python str allocation. Cap per-column-name at 4 KiB, matching the defense-in-depth pattern used for FailureResponse.message. Legitimate SQL identifiers are orders of magnitude smaller.
1 parent 8b03bec commit 17cd924

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
# above any realistic message so legitimate cases are never clipped.
4848
_MAX_FAILURE_MESSAGE_SIZE = 64 * 1024
4949

50+
# Per-column-name cap on ``RowsResponse``. SQLite column-name identifiers
51+
# are short by any realistic standard; 4 KiB is orders of magnitude above
52+
# legitimate use and well below any memory-exhaustion concern. Same
53+
# defense-in-depth policy as ``_MAX_FAILURE_MESSAGE_SIZE``.
54+
_MAX_COLUMN_NAME_SIZE = 4096
55+
5056
# Sanitize server-supplied text destined for exception messages and
5157
# logs. The C server promises UTF-8 but makes no promise about terminal
5258
# escapes or log-injection characters: a malicious or compromised peer
@@ -424,6 +430,10 @@ def decode_body(
424430
column_names: list[str] = []
425431
for _ in range(column_count):
426432
name, consumed = decode_text(view[offset:])
433+
if len(name) > _MAX_COLUMN_NAME_SIZE:
434+
raise DecodeError(
435+
f"column name length {len(name)} exceeds maximum {_MAX_COLUMN_NAME_SIZE}"
436+
)
427437
column_names.append(name)
428438
offset += consumed
429439

tests/test_messages_responses.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from dqlitewire.exceptions import DecodeError, EncodeError
77
from dqlitewire.messages.base import Header
88
from dqlitewire.messages.responses import (
9+
_MAX_COLUMN_NAME_SIZE,
910
_MAX_FAILURE_MESSAGE_SIZE,
1011
DbResponse,
1112
EmptyResponse,
@@ -768,6 +769,35 @@ def test_zero_columns_with_rows_raises(self) -> None:
768769
resp.encode_body()
769770

770771

772+
class TestRowsResponseColumnNameSize:
773+
"""Per-column-name length cap in RowsResponse decode.
774+
775+
The outer 64 MiB frame cap is the ultimate backstop, but a peer can
776+
still pack a single giant column name inside a frame-legal response
777+
and force the client to allocate it as a Python string. Cap each
778+
column name at ``_MAX_COLUMN_NAME_SIZE`` (same policy as
779+
``_MAX_FAILURE_MESSAGE_SIZE``).
780+
"""
781+
782+
def _build_body(self, name: str) -> bytes:
783+
# One-column RowsResponse, no rows, DONE marker.
784+
from dqlitewire.constants import ROW_DONE_MARKER
785+
786+
return encode_uint64(1) + encode_text(name) + encode_uint64(ROW_DONE_MARKER)
787+
788+
def test_decode_rejects_oversize_column_name(self) -> None:
789+
oversize = "a" * (_MAX_COLUMN_NAME_SIZE + 1)
790+
body = self._build_body(oversize)
791+
with pytest.raises(DecodeError, match="column name"):
792+
RowsResponse.decode_body(body)
793+
794+
def test_decode_accepts_column_name_at_cap(self) -> None:
795+
at_cap = "a" * _MAX_COLUMN_NAME_SIZE
796+
body = self._build_body(at_cap)
797+
decoded = RowsResponse.decode_body(body)
798+
assert decoded.column_names == [at_cap]
799+
800+
771801
class TestRowsResponseNullInTypedColumn:
772802
"""137: None values in rows with explicit column types must encode correctly."""
773803

0 commit comments

Comments
 (0)