Skip to content

Commit d661830

Browse files
Cap filename length in FilesResponse decoder
Mirror of the column-name cap: bound per-filename decode to _MAX_FILENAME_SIZE (4 KiB, matching POSIX PATH_MAX) so a frame-legal FilesResponse cannot force unbounded string allocation through a single giant filename entry.
1 parent 17cd924 commit d661830

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@
5353
# defense-in-depth policy as ``_MAX_FAILURE_MESSAGE_SIZE``.
5454
_MAX_COLUMN_NAME_SIZE = 4096
5555

56+
# Per-filename cap on ``FilesResponse``. dqlite file entries are the
57+
# on-disk page-backed database files (``main``, ``wal``, etc.); POSIX
58+
# PATH_MAX is 4 KiB and mirrors the column-name cap.
59+
_MAX_FILENAME_SIZE = 4096
60+
5661
# Sanitize server-supplied text destined for exception messages and
5762
# logs. The C server promises UTF-8 but makes no promise about terminal
5863
# escapes or log-injection characters: a malicious or compromised peer
@@ -590,6 +595,10 @@ def decode_body(cls, data: bytes, schema: int = 0) -> "FilesResponse":
590595
)
591596
for _ in range(count):
592597
name, consumed = decode_text(view[offset:])
598+
if len(name) > _MAX_FILENAME_SIZE:
599+
raise DecodeError(
600+
f"filename length {len(name)} exceeds maximum {_MAX_FILENAME_SIZE}"
601+
)
593602
offset += consumed
594603
size = decode_uint64(view[offset:])
595604
offset += 8

tests/test_messages_responses.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dqlitewire.messages.responses import (
99
_MAX_COLUMN_NAME_SIZE,
1010
_MAX_FAILURE_MESSAGE_SIZE,
11+
_MAX_FILENAME_SIZE,
1112
DbResponse,
1213
EmptyResponse,
1314
FailureResponse,
@@ -1225,6 +1226,32 @@ def test_truncated_file_content_raises(self) -> None:
12251226
FilesResponse.decode_body(body)
12261227

12271228

1229+
class TestFilesResponseFilenameSize:
1230+
"""Per-filename length cap in FilesResponse decode.
1231+
1232+
The outer 64 MiB frame cap bounds total bytes, but a peer can still
1233+
pack a giant filename in a frame-legal FilesResponse. Cap each
1234+
filename at ``_MAX_FILENAME_SIZE`` (POSIX PATH_MAX convention).
1235+
"""
1236+
1237+
def _build_body(self, name: str) -> bytes:
1238+
# Single entry with word-aligned content.
1239+
content = b"\x00" * 8
1240+
return encode_uint64(1) + encode_text(name) + encode_uint64(len(content)) + content
1241+
1242+
def test_decode_rejects_oversize_filename(self) -> None:
1243+
oversize = "a" * (_MAX_FILENAME_SIZE + 1)
1244+
body = self._build_body(oversize)
1245+
with pytest.raises(DecodeError, match="filename"):
1246+
FilesResponse.decode_body(body)
1247+
1248+
def test_decode_accepts_filename_at_cap(self) -> None:
1249+
at_cap = "a" * _MAX_FILENAME_SIZE
1250+
body = self._build_body(at_cap)
1251+
decoded = FilesResponse.decode_body(body)
1252+
assert at_cap in decoded.files
1253+
1254+
12281255
class TestServersResponse:
12291256
def test_empty(self) -> None:
12301257
msg = ServersResponse(nodes=[])

0 commit comments

Comments
 (0)