-
Notifications
You must be signed in to change notification settings - Fork 16k
Description
What version of protobuf and what language are you using?
Version: main (c73d13f)
Language: Python
What operating system (Linux, Windows, ...) and version?
Ubuntu 25.04
Hi!
I noticed there are some recursion depth limit bypasses similar to some of the recently reported issues in this repository (e.g., #25070) that could also lead to denial-of-service attacks under specific configurations if exploited.
In the pure Python protobuf runtime, two decoder paths drop current_depth when calling
_InternalParse, resetting recursion accounting and bypassing SetRecursionLimit(...). Crafted
inputs can then hit Python's recursion limit (RecursionError), causing a DoS.
Affected
- Pure Python runtime only (
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python). - Binary parsing via
ParseFromString()/MergeFromString(). - Map fields with message values.
- Proto2 MessageSet (
message_set_wire_format = trueand extensions).
Root cause
Map fields:
python/google/protobuf/internal/decoder.py:959deletescurrent_depth.python/google/protobuf/internal/decoder.py:973callssubmsg._InternalParse(...)without depth.
MessageSet:
python/google/protobuf/internal/python_message.py:1240and
python/google/protobuf/internal/python_message.py:1244dispatch MessageSet decoders without
depth.python/google/protobuf/internal/decoder.py:891callsvalue._InternalParse(...)without depth.
Depth defaults to 0 in _InternalParse: python/google/protobuf/internal/python_message.py:1221.
Impact
Attacker-controlled nested payloads can exceed configured recursion limits and raise
RecursionError in the pure-Python runtime. C++/upb runtimes are unaffected.
Reproduction
Place PoCs in python/poc (Rename the .txt files to .proto):
- Map: recursive_map.proto, repro_recursion.py
- MessageSet: message_set_recursion.proto, repro_message_set_recursion.py
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
mkdir -p python/poc
bazel build //:protoc //python:python_src_files
# Regenerate files, might not be needed
cp bazel-bin/python/google/protobuf/internal/python_edition_defaults.py \
python/google/protobuf/internal/
./bazel-bin/protoc --python_out=/tmp -I src src/google/protobuf/descriptor.proto
cp /tmp/google/protobuf/descriptor_pb2.py python/google/protobuf/
./bazel-bin/protoc --python_out=python/poc -I python/poc python/poc/recursive_map.proto
./bazel-bin/protoc --python_out=python/poc -I python/poc python/poc/message_set_recursion.proto
PYTHONPATH=python PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python \
python python/poc/repro_recursion.py --depth 3000 --recursion-limit 50
PYTHONPATH=python PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python \
python python/poc/repro_message_set_recursion.py --depth 3000 --recursion-limit 50Expected: both runs raise RecursionError despite a low SetRecursionLimit.
Suggested fix
Thread current_depth through both decoder paths and increment before parsing nested messages:
- Map decoder: stop deleting
current_depth, pass it intosubmsg._InternalParse(..., current_depth). - MessageSet path: pass depth into the MessageSet field decoder and into
value._InternalParse(..., current_depth).
Objective-C
I think a similar MessageSet recursion-depth-tracking bypass might be present in the Objective-C implementation, but I don't know the language well enough and don't have access to a device running macOS to reproduce it.
Execution path:
MessageSet parsing in mergeFromCodedInputStream dispatches to parseMessageSet.
objectivec/GPBMessage.m:2695objectivec/GPBMessage.m:2697
parseMessageSetbuffers raw bytes and constructs a newGPBCodedInputStreamto parse the extension message, without updating recursion depth.objectivec/GPBMessage.m:2387objectivec/GPBMessage.m:2389