Skip to content

Python: More missing recursion guards #25335

@0Zeta

Description

@0Zeta

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 = true and extensions).

Root cause

Map fields:

  • python/google/protobuf/internal/decoder.py:959 deletes current_depth.
  • python/google/protobuf/internal/decoder.py:973 calls submsg._InternalParse(...) without depth.

MessageSet:

  • python/google/protobuf/internal/python_message.py:1240 and
    python/google/protobuf/internal/python_message.py:1244 dispatch MessageSet decoders without
    depth.
  • python/google/protobuf/internal/decoder.py:891 calls value._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):

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 50

Expected: 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 into submsg._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:2695
  • objectivec/GPBMessage.m:2697
    parseMessageSet buffers raw bytes and constructs a new GPBCodedInputStream to parse the extension message, without updating recursion depth.
  • objectivec/GPBMessage.m:2387
  • objectivec/GPBMessage.m:2389

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions