Skip to content

Commit 32859c2

Browse files
C1-BA-B1-F3Sisyphus
authored andcommitted
fix: guard RESP parsers against RecursionError on deeply nested replies
The pure-Python RESP parser path uses recursive parsing for nested aggregate replies (arrays, sets, maps, push). A malicious server or compromised proxy can inject deeply nested RESP replies to trigger RecursionError, causing client-side denial of service. Add MAX_NESTING_DEPTH (100) to all four parser classes: - _RESP2Parser / _AsyncRESP2Parser - _RESP3Parser / _AsyncRESP3Parser When nesting depth is exceeded, raise InvalidResponse instead of allowing unbounded recursion. Fixes #4116
1 parent bc0c81b commit 32859c2

3 files changed

Lines changed: 359 additions & 13 deletions

File tree

redis/_parsers/resp2.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
class _RESP2Parser(_RESPBase):
1111
"""RESP2 protocol implementation"""
1212

13+
MAX_NESTING_DEPTH = 100
14+
1315
def read_response(
1416
self, disable_decoding=False, timeout: Union[float, object] = SENTINEL
1517
):
@@ -27,7 +29,10 @@ def read_response(
2729
return result
2830

2931
def _read_response(
30-
self, disable_decoding=False, timeout: Union[float, object] = SENTINEL
32+
self,
33+
disable_decoding=False,
34+
timeout: Union[float, object] = SENTINEL,
35+
_depth=0,
3136
):
3237
raw = self._buffer.readline(timeout=timeout)
3338
if not raw:
@@ -63,8 +68,16 @@ def _read_response(
6368
elif byte == b"*" and response == b"-1":
6469
return None
6570
elif byte == b"*":
71+
if _depth >= self.MAX_NESTING_DEPTH:
72+
raise InvalidResponse(
73+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
74+
)
6675
response = [
67-
self._read_response(disable_decoding=disable_decoding, timeout=timeout)
76+
self._read_response(
77+
disable_decoding=disable_decoding,
78+
timeout=timeout,
79+
_depth=_depth + 1,
80+
)
6881
for i in range(int(response))
6982
]
7083
else:
@@ -78,6 +91,8 @@ def _read_response(
7891
class _AsyncRESP2Parser(_AsyncRESPBase):
7992
"""Async class for the RESP2 protocol"""
8093

94+
MAX_NESTING_DEPTH = 100
95+
8196
async def read_response(self, disable_decoding: bool = False):
8297
if not self._connected:
8398
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
@@ -92,7 +107,7 @@ async def read_response(self, disable_decoding: bool = False):
92107
return response
93108

94109
async def _read_response(
95-
self, disable_decoding: bool = False
110+
self, disable_decoding: bool = False, _depth: int = 0
96111
) -> Union[EncodableT, ResponseError, None]:
97112
raw = await self._readline()
98113
response: Any
@@ -127,8 +142,12 @@ async def _read_response(
127142
elif byte == b"*" and response == b"-1":
128143
return None
129144
elif byte == b"*":
145+
if _depth >= self.MAX_NESTING_DEPTH:
146+
raise InvalidResponse(
147+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
148+
)
130149
response = [
131-
(await self._read_response(disable_decoding))
150+
(await self._read_response(disable_decoding, _depth=_depth + 1))
132151
for _ in range(int(response)) # noqa
133152
]
134153
else:

redis/_parsers/resp3.py

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
class _RESP3Parser(_RESPBase, PushNotificationsParser):
1717
"""RESP3 protocol implementation"""
1818

19+
MAX_NESTING_DEPTH = 100
20+
1921
def __init__(self, socket_read_size):
2022
super().__init__(socket_read_size)
2123
self.pubsub_push_handler_func = self.handle_pubsub_push_response
@@ -61,6 +63,7 @@ def _read_response(
6163
disable_decoding=False,
6264
push_request=False,
6365
timeout: Union[float, object] = SENTINEL,
66+
_depth=0,
6467
):
6568
raw = self._buffer.readline(timeout=timeout)
6669
if not raw:
@@ -106,41 +109,69 @@ def _read_response(
106109
response = self._buffer.read(int(response), timeout=timeout)[4:]
107110
# array response
108111
elif byte == b"*":
112+
if _depth >= self.MAX_NESTING_DEPTH:
113+
raise InvalidResponse(
114+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
115+
)
109116
response = [
110-
self._read_response(disable_decoding=disable_decoding, timeout=timeout)
117+
self._read_response(
118+
disable_decoding=disable_decoding,
119+
timeout=timeout,
120+
_depth=_depth + 1,
121+
)
111122
for _ in range(int(response))
112123
]
113124
# set response
114125
elif byte == b"~":
126+
if _depth >= self.MAX_NESTING_DEPTH:
127+
raise InvalidResponse(
128+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
129+
)
115130
# redis can return unhashable types (like dict) in a set,
116131
# so we return sets as list, all the time, for predictability
117132
response = [
118-
self._read_response(disable_decoding=disable_decoding, timeout=timeout)
133+
self._read_response(
134+
disable_decoding=disable_decoding,
135+
timeout=timeout,
136+
_depth=_depth + 1,
137+
)
119138
for _ in range(int(response))
120139
]
121140
# map response
122141
elif byte == b"%":
142+
if _depth >= self.MAX_NESTING_DEPTH:
143+
raise InvalidResponse(
144+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
145+
)
123146
# We cannot use a dict-comprehension to parse stream.
124147
# Evaluation order of key:val expression in dict comprehension only
125148
# became defined to be left-right in version 3.8
126149
resp_dict = {}
127150
for _ in range(int(response)):
128151
key = self._read_response(
129-
disable_decoding=disable_decoding, timeout=timeout
152+
disable_decoding=disable_decoding,
153+
timeout=timeout,
154+
_depth=_depth + 1,
130155
)
131156
resp_dict[key] = self._read_response(
132157
disable_decoding=disable_decoding,
133158
push_request=push_request,
134159
timeout=timeout,
160+
_depth=_depth + 1,
135161
)
136162
response = resp_dict
137163
# push response
138164
elif byte == b">":
165+
if _depth >= self.MAX_NESTING_DEPTH:
166+
raise InvalidResponse(
167+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
168+
)
139169
response = [
140170
self._read_response(
141171
disable_decoding=disable_decoding,
142172
push_request=push_request,
143173
timeout=timeout,
174+
_depth=_depth + 1,
144175
)
145176
for _ in range(int(response))
146177
]
@@ -164,6 +195,8 @@ def _read_response(
164195

165196

166197
class _AsyncRESP3Parser(_AsyncRESPBase, AsyncPushNotificationsParser):
198+
MAX_NESTING_DEPTH = 100
199+
167200
def __init__(self, socket_read_size):
168201
super().__init__(socket_read_size)
169202
self.pubsub_push_handler_func = self.handle_pubsub_push_response
@@ -190,7 +223,7 @@ async def read_response(
190223
return response
191224

192225
async def _read_response(
193-
self, disable_decoding: bool = False, push_request: bool = False
226+
self, disable_decoding: bool = False, push_request: bool = False, _depth: int = 0
194227
) -> Union[EncodableT, ResponseError, None]:
195228
if not self._stream or not self.encoder:
196229
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
@@ -240,36 +273,54 @@ async def _read_response(
240273
response = (await self._read(int(response)))[4:]
241274
# array response
242275
elif byte == b"*":
276+
if _depth >= self.MAX_NESTING_DEPTH:
277+
raise InvalidResponse(
278+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
279+
)
243280
response = [
244-
(await self._read_response(disable_decoding=disable_decoding))
281+
(await self._read_response(disable_decoding, _depth=_depth + 1))
245282
for _ in range(int(response))
246283
]
247284
# set response
248285
elif byte == b"~":
286+
if _depth >= self.MAX_NESTING_DEPTH:
287+
raise InvalidResponse(
288+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
289+
)
249290
# redis can return unhashable types (like dict) in a set,
250291
# so we always convert to a list, to have predictable return types
251292
response = [
252-
(await self._read_response(disable_decoding=disable_decoding))
293+
(await self._read_response(disable_decoding, _depth=_depth + 1))
253294
for _ in range(int(response))
254295
]
255296
# map response
256297
elif byte == b"%":
298+
if _depth >= self.MAX_NESTING_DEPTH:
299+
raise InvalidResponse(
300+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
301+
)
257302
# We cannot use a dict-comprehension to parse stream.
258303
# Evaluation order of key:val expression in dict comprehension only
259304
# became defined to be left-right in version 3.8
260305
resp_dict = {}
261306
for _ in range(int(response)):
262-
key = await self._read_response(disable_decoding=disable_decoding)
307+
key = await self._read_response(
308+
disable_decoding, _depth=_depth + 1
309+
)
263310
resp_dict[key] = await self._read_response(
264-
disable_decoding=disable_decoding, push_request=push_request
311+
disable_decoding, push_request=push_request, _depth=_depth + 1
265312
)
266313
response = resp_dict
267314
# push response
268315
elif byte == b">":
316+
if _depth >= self.MAX_NESTING_DEPTH:
317+
raise InvalidResponse(
318+
f"Response nesting depth exceeded {self.MAX_NESTING_DEPTH}"
319+
)
269320
response = [
270321
(
271322
await self._read_response(
272-
disable_decoding=disable_decoding, push_request=push_request
323+
disable_decoding, push_request=push_request, _depth=_depth + 1
273324
)
274325
)
275326
for _ in range(int(response))

0 commit comments

Comments
 (0)