Skip to content

Commit 7743347

Browse files
Bound out-of-range error message in int encoders
A pathologically large int passed to encode_uint64, encode_int64, or encode_uint32 baked a kilobyte of digits into the EncodeError message, bloating every log line and traceback that quoted it. Truncate the stringified value to 64 digits with a "... [N digits]" tail, parity with _truncate_error in client.cluster and the failure-response cap already present on the decode side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b38de06 commit 7743347

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

src/dqlitewire/types.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,26 @@
2626
# ``_MAX_FILE_COUNT`` / ``_MAX_NODE_COUNT`` in spirit.
2727
_MAX_BLOB_SIZE = 16 * 1024 * 1024 # 16 MiB
2828

29+
# Cap on the stringified representation of an out-of-range integer in
30+
# EncodeError messages. A hostile or buggy caller passing ``10 ** 500``
31+
# would otherwise bake a kilobyte of digits into the error text (and
32+
# every log line / traceback that quotes it). Parity with
33+
# ``_truncate_error`` in the client layer and ``_MAX_FAILURE_MESSAGE_SIZE``
34+
# on the decode side.
35+
_MAX_VALUE_REPR = 64
36+
37+
38+
def _bounded_repr(value: int) -> str:
39+
s = str(value)
40+
if len(s) <= _MAX_VALUE_REPR:
41+
return s
42+
return f"{s[:_MAX_VALUE_REPR]}... [{len(s)} digits]"
43+
2944

3045
def encode_uint64(value: int) -> bytes:
3146
"""Encode an unsigned 64-bit integer (little-endian)."""
3247
if not 0 <= value < 2**64:
33-
raise EncodeError(f"Value {value} out of range for uint64")
48+
raise EncodeError(f"Value {_bounded_repr(value)} out of range for uint64")
3449
return struct.pack("<Q", value)
3550

3651

@@ -49,7 +64,7 @@ def decode_uint64(data: bytes | memoryview) -> int:
4964
def encode_int64(value: int) -> bytes:
5065
"""Encode a signed 64-bit integer (little-endian)."""
5166
if not -(2**63) <= value < 2**63:
52-
raise EncodeError(f"Value {value} out of range for int64")
67+
raise EncodeError(f"Value {_bounded_repr(value)} out of range for int64")
5368
return struct.pack("<q", value)
5469

5570

@@ -67,7 +82,7 @@ def decode_int64(data: bytes | memoryview) -> int:
6782
def encode_uint32(value: int) -> bytes:
6883
"""Encode an unsigned 32-bit integer (little-endian)."""
6984
if not 0 <= value < 2**32:
70-
raise EncodeError(f"Value {value} out of range for uint32")
85+
raise EncodeError(f"Value {_bounded_repr(value)} out of range for uint32")
7186
return struct.pack("<I", value)
7287

7388

tests/test_types.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,35 @@ def test_encode_underflow_fails(self) -> None:
8484
with pytest.raises(EncodeError):
8585
encode_int64(-(2**63) - 1)
8686

87+
def test_encode_overflow_error_message_is_bounded(self) -> None:
88+
"""A pathologically large int must not produce a multi-kB error
89+
message. Parity with _truncate_error in client.cluster and the
90+
failure-response cap in responses.py: attacker-or-bug-controlled
91+
input should surface a bounded diagnostic, not a kilobyte of
92+
digits.
93+
"""
94+
huge = 10**500
95+
with pytest.raises(EncodeError) as exc_info:
96+
encode_int64(huge)
97+
assert len(str(exc_info.value)) < 256
98+
assert "out of range for int64" in str(exc_info.value)
99+
100+
101+
class TestOverflowMessageBounds:
102+
"""Cross-encoder parity for bounded overflow error messages."""
103+
104+
def test_uint64_overflow_message_bounded(self) -> None:
105+
with pytest.raises(EncodeError) as exc_info:
106+
encode_uint64(10**500)
107+
assert len(str(exc_info.value)) < 256
108+
assert "out of range for uint64" in str(exc_info.value)
109+
110+
def test_uint32_overflow_message_bounded(self) -> None:
111+
with pytest.raises(EncodeError) as exc_info:
112+
encode_uint32(10**500)
113+
assert len(str(exc_info.value)) < 256
114+
assert "out of range for uint32" in str(exc_info.value)
115+
87116

88117
class TestUint32:
89118
def test_encode_zero(self) -> None:

0 commit comments

Comments
 (0)