fix(protocols): guard against zero/malformed length fields in binary protocol parsers
MongoDB had the same infinite-loop bug as MSSQL (msg_len=0 → buffer never shrinks in while loop). Postgres, MySQL, and MQTT had related length-field issues (stuck state, resource exhaustion, overlong remaining-length). Also fixes an existing MongoDB _op_reply struct.pack format bug (extra 'q' specifier caused struct.error on any OP_QUERY response). Adds 53 regression + protocol boundary tests across MSSQL, MongoDB, Postgres, MySQL, and MQTT, including a _run_with_timeout threading harness to catch infinite loops and @pytest.mark.fuzz hypothesis tests for each.
This commit is contained in:
@@ -35,13 +35,13 @@ def _op_reply(request_id: int, doc: bytes) -> bytes:
|
||||
# OP_REPLY header: total_len(4), req_id(4), response_to(4), opcode(4)=1,
|
||||
# flags(4), cursor_id(8), starting_from(4), number_returned(4), docs
|
||||
header = struct.pack(
|
||||
"<iiiiiqqii",
|
||||
"<iiiiiqii",
|
||||
16 + 20 + len(doc), # total length
|
||||
0, # request id
|
||||
request_id, # response to
|
||||
1, # OP_REPLY
|
||||
0, # flags
|
||||
0, # cursor id
|
||||
0, # cursor id (int64)
|
||||
0, # starting from
|
||||
1, # number returned
|
||||
)
|
||||
@@ -81,6 +81,10 @@ class MongoDBProtocol(asyncio.Protocol):
|
||||
self._buf += data
|
||||
while len(self._buf) >= 16:
|
||||
msg_len = struct.unpack("<I", self._buf[:4])[0]
|
||||
if msg_len < 16 or msg_len > 48 * 1024 * 1024:
|
||||
self._transport.close()
|
||||
self._buf = b""
|
||||
return
|
||||
if len(self._buf) < msg_len:
|
||||
break
|
||||
msg = self._buf[:msg_len]
|
||||
|
||||
@@ -191,6 +191,10 @@ class MQTTProtocol(asyncio.Protocol):
|
||||
remaining = 0
|
||||
multiplier = 1
|
||||
while pos < len(self._buf):
|
||||
if pos > 4: # MQTT spec: max 4 bytes for remaining length
|
||||
self._transport.close()
|
||||
self._buf = b""
|
||||
return
|
||||
byte = self._buf[pos]
|
||||
remaining += (byte & 0x7f) * multiplier
|
||||
multiplier *= 128
|
||||
|
||||
@@ -67,6 +67,10 @@ class MySQLProtocol(asyncio.Protocol):
|
||||
# MySQL packets: 3-byte length + 1-byte seq + payload
|
||||
while len(self._buf) >= 4:
|
||||
length = struct.unpack("<I", self._buf[:3] + b"\x00")[0]
|
||||
if length > 1024 * 1024:
|
||||
self._transport.close()
|
||||
self._buf = b""
|
||||
return
|
||||
if len(self._buf) < 4 + length:
|
||||
break
|
||||
payload = self._buf[4:4 + length]
|
||||
|
||||
@@ -49,6 +49,10 @@ class PostgresProtocol(asyncio.Protocol):
|
||||
if len(self._buf) < 4:
|
||||
return
|
||||
msg_len = struct.unpack(">I", self._buf[:4])[0]
|
||||
if msg_len < 8 or msg_len > 10_000:
|
||||
self._transport.close()
|
||||
self._buf = b""
|
||||
return
|
||||
if len(self._buf) < msg_len:
|
||||
return
|
||||
msg = self._buf[:msg_len]
|
||||
@@ -59,6 +63,10 @@ class PostgresProtocol(asyncio.Protocol):
|
||||
return
|
||||
msg_type = chr(self._buf[0])
|
||||
msg_len = struct.unpack(">I", self._buf[1:5])[0]
|
||||
if msg_len < 4 or msg_len > 10_000:
|
||||
self._transport.close()
|
||||
self._buf = b""
|
||||
return
|
||||
if len(self._buf) < msg_len + 1:
|
||||
return
|
||||
payload = self._buf[5:msg_len + 1]
|
||||
|
||||
Reference in New Issue
Block a user