Skip to content

Commit 8daee9d

Browse files
Cap blob size at encoder and decoder
The blob decoder previously computed total_size = 8 + length + pad_to_word(length) from an attacker-controlled uint64 length before the bounds check. A peer claiming length=2**62 forces the decoder to do the arithmetic (and later allocation up to the 64 MiB frame cap) before rejecting. Add a per-field _MAX_BLOB_SIZE = 16 MiB cap, enforced at decode before any arithmetic with length and mirrored at encode for symmetry with _MAX_PARAM_COUNT in tuples.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4625b15 commit 8daee9d

2 files changed

Lines changed: 39 additions & 0 deletions

File tree

src/dqlitewire/types.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
from dqlitewire.constants import WORD_SIZE, ValueType
1717
from dqlitewire.exceptions import DecodeError, EncodeError
1818

19+
# Per-BLOB byte cap. The overall frame-size cap in ``buffer.py`` (64 MiB)
20+
# already bounds any single message, but a hostile or buggy peer can
21+
# otherwise pack a single BLOB field that consumes the whole frame. The
22+
# cap is a defensive ceiling — real applications do not send
23+
# multi-megabyte blobs over the wire — and keeps the decoder fast-failing
24+
# well before large allocations or arithmetic on attacker-controlled
25+
# lengths. Sits beside ``_MAX_PARAM_COUNT`` / ``_MAX_COLUMN_COUNT`` /
26+
# ``_MAX_FILE_COUNT`` / ``_MAX_NODE_COUNT`` in spirit.
27+
_MAX_BLOB_SIZE = 16 * 1024 * 1024 # 16 MiB
28+
1929

2030
def encode_uint64(value: int) -> bytes:
2131
"""Encode an unsigned 64-bit integer (little-endian)."""
@@ -202,6 +212,8 @@ def encode_blob(value: bytes) -> bytes:
202212
Format: uint64 length + data + padding
203213
"""
204214
length = len(value)
215+
if length > _MAX_BLOB_SIZE:
216+
raise EncodeError(f"Blob length {length} exceeds maximum ({_MAX_BLOB_SIZE})")
205217
padding = pad_to_word(length)
206218
return encode_uint64(length) + value + (b"\x00" * padding)
207219

@@ -216,6 +228,8 @@ def decode_blob(data: bytes | memoryview) -> tuple[bytes, int]:
216228
raise DecodeError("Not enough data for blob length")
217229

218230
length = decode_uint64(data[:8])
231+
if length > _MAX_BLOB_SIZE:
232+
raise DecodeError(f"Blob length {length} exceeds maximum ({_MAX_BLOB_SIZE})")
219233
total_size = 8 + length + pad_to_word(length)
220234

221235
if len(data) < total_size:

tests/test_types.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dqlitewire.constants import ValueType
88
from dqlitewire.exceptions import DecodeError, EncodeError
99
from dqlitewire.types import (
10+
_MAX_BLOB_SIZE,
1011
decode_blob,
1112
decode_double,
1213
decode_int64,
@@ -304,6 +305,30 @@ def test_decode_blob_truncated_data(self) -> None:
304305
with pytest.raises(DecodeError, match="Not enough data for blob"):
305306
decode_blob(data)
306307

308+
def test_decode_blob_rejects_length_beyond_cap(self) -> None:
309+
"""Crafted buffer claiming a length beyond the per-field cap must be
310+
rejected before the decoder allocates or does total-size arithmetic
311+
with the attacker-controlled length."""
312+
oversized = _MAX_BLOB_SIZE + 1
313+
data = encode_uint64(oversized)
314+
with pytest.raises(DecodeError, match="exceeds maximum"):
315+
decode_blob(data)
316+
317+
def test_decode_blob_accepts_length_at_cap(self) -> None:
318+
"""Length exactly at the cap is still accepted; the body check is
319+
what fires on a truncated buffer."""
320+
# Claim exactly the cap but provide a short buffer. The cap check
321+
# must pass; the later "not enough data" check is what rejects.
322+
data = encode_uint64(_MAX_BLOB_SIZE) + b"\x00" * 8
323+
with pytest.raises(DecodeError, match="Not enough data for blob"):
324+
decode_blob(data)
325+
326+
def test_encode_blob_rejects_oversize(self) -> None:
327+
"""encode_blob mirrors the decode cap so callers fail fast on an
328+
accidental giant bytes input instead of burning allocations."""
329+
with pytest.raises(EncodeError, match="exceeds maximum"):
330+
encode_blob(b"\x00" * (_MAX_BLOB_SIZE + 1))
331+
307332

308333
class TestValue:
309334
def test_encode_integer(self) -> None:

0 commit comments

Comments
 (0)