Skip to content

Commit f7bb86e

Browse files
Cap FailureResponse message at encoder and decoder
A peer returning a multi-megabyte error message forces an allocation and a full-string C0-sanitize scan on every FAILURE response. Real SQLite error strings are under a few hundred characters; anything beyond the new 64 KiB cap is malicious or broken. Cap at both encode and decode, consistent with _MAX_COLUMN_COUNT / _MAX_FILE_COUNT / _MAX_NODE_COUNT in the same file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8daee9d commit f7bb86e

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

src/dqlitewire/messages/responses.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
_MAX_FILE_COUNT = 100
4141
_MAX_NODE_COUNT = 10_000
4242

43+
# Per-field cap on ``FailureResponse.message``. The frame-size cap
44+
# in ``buffer.py`` (64 MiB) bounds total bytes, but error messages in
45+
# practice are short (SQLite's own error strings are under ~200 chars).
46+
# A peer sending megabytes of text is malicious or broken; cap well
47+
# above any realistic message so legitimate cases are never clipped.
48+
_MAX_FAILURE_MESSAGE_SIZE = 64 * 1024
49+
4350
# Sanitize server-supplied text destined for exception messages and
4451
# logs. The C server promises UTF-8 but makes no promise about terminal
4552
# escapes or log-injection characters: a malicious or compromised peer
@@ -84,12 +91,21 @@ class FailureResponse(Message):
8491
message: str
8592

8693
def encode_body(self) -> bytes:
94+
if len(self.message) > _MAX_FAILURE_MESSAGE_SIZE:
95+
raise EncodeError(
96+
f"Failure message length {len(self.message)} "
97+
f"exceeds maximum {_MAX_FAILURE_MESSAGE_SIZE}"
98+
)
8799
return encode_uint64(self.code) + encode_text(self.message)
88100

89101
@classmethod
90102
def decode_body(cls, data: bytes, schema: int = 0) -> "FailureResponse":
91103
code = decode_uint64(data)
92104
message, _ = decode_text(data[8:])
105+
if len(message) > _MAX_FAILURE_MESSAGE_SIZE:
106+
raise DecodeError(
107+
f"Failure message length {len(message)} exceeds maximum {_MAX_FAILURE_MESSAGE_SIZE}"
108+
)
93109
return cls(code, _sanitize_server_text(message))
94110

95111

tests/test_messages_responses.py

Lines changed: 27 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_FAILURE_MESSAGE_SIZE,
910
DbResponse,
1011
EmptyResponse,
1112
FailureResponse,
@@ -19,6 +20,7 @@
1920
StmtResponse,
2021
WelcomeResponse,
2122
)
23+
from dqlitewire.types import encode_text, encode_uint64
2224

2325

2426
class TestFailureResponse:
@@ -42,6 +44,31 @@ def test_empty_message(self) -> None:
4244
assert decoded.code == 0
4345
assert decoded.message == ""
4446

47+
def test_decode_rejects_oversize_message(self) -> None:
48+
"""A peer claiming a multi-megabyte error message can force a large
49+
allocation and full-string scan through _sanitize_server_text. Cap
50+
the decoded message at _MAX_FAILURE_MESSAGE_SIZE so the decoder
51+
fails fast before the sanitize scan runs."""
52+
oversize = "a" * (_MAX_FAILURE_MESSAGE_SIZE + 1)
53+
body = encode_uint64(1) + encode_text(oversize)
54+
with pytest.raises(DecodeError, match="exceeds maximum"):
55+
FailureResponse.decode_body(body)
56+
57+
def test_decode_accepts_message_at_cap(self) -> None:
58+
"""Exactly-cap message must still decode."""
59+
at_cap = "a" * _MAX_FAILURE_MESSAGE_SIZE
60+
body = encode_uint64(1) + encode_text(at_cap)
61+
decoded = FailureResponse.decode_body(body)
62+
assert decoded.code == 1
63+
assert len(decoded.message) == _MAX_FAILURE_MESSAGE_SIZE
64+
65+
def test_encode_rejects_oversize_message(self) -> None:
66+
"""Encoder mirrors the decode cap so callers fail fast on an
67+
accidentally-huge message string."""
68+
msg = FailureResponse(code=1, message="a" * (_MAX_FAILURE_MESSAGE_SIZE + 1))
69+
with pytest.raises(EncodeError, match="exceeds maximum"):
70+
msg.encode_body()
71+
4572

4673
class TestLeaderResponse:
4774
def test_roundtrip(self) -> None:

0 commit comments

Comments
 (0)