Internal change

PiperOrigin-RevId: 971393382
diff --git a/php/src/Google/Protobuf/Internal/GPBJsonWire.php b/php/src/Google/Protobuf/Internal/GPBJsonWire.php
index 08f6fd9..4969519 100644
--- a/php/src/Google/Protobuf/Internal/GPBJsonWire.php
+++ b/php/src/Google/Protobuf/Internal/GPBJsonWire.php
@@ -17,7 +17,7 @@
     public static function serializeFieldToStream(
         $value,
         $field,
-        &$output, $has_field_name = true)
+        &$output, $has_field_name = true, $depth = 0)
     {
         if ($has_field_name) {
             $output->writeRaw("\"", 1);
@@ -29,14 +29,16 @@
             $value,
             $field,
             $output,
-            !$has_field_name);
+            !$has_field_name,
+            $depth);
     }
 
     public static function serializeFieldValueToStream(
         $values,
         $field,
         &$output,
-        $is_well_known = false)
+        $is_well_known = false,
+        $depth = 0)
     {
         if ($field->isMap()) {
             $output->writeRaw("{", 1);
@@ -71,7 +73,8 @@
                     $key,
                     $key_field,
                     $output,
-                    $is_well_known)) {
+                    $is_well_known,
+                    $depth)) {
                     return false;
                 }
                 if ($additional_quote) {
@@ -82,7 +85,8 @@
                     $value,
                     $value_field,
                     $output,
-                    $is_well_known)) {
+                    $is_well_known,
+                    $depth)) {
                     return false;
                 }
             }
@@ -101,7 +105,8 @@
                     $value,
                     $field,
                     $output,
-                    $is_well_known)) {
+                    $is_well_known,
+                    $depth)) {
                     return false;
                 }
             }
@@ -112,14 +117,15 @@
                 $values,
                 $field,
                 $output,
-                $is_well_known);
+                $is_well_known,
+                $depth);
         }
     }
 
     private static function serializeSingularFieldValueToStream(
         $value,
         $field,
-        &$output, $is_well_known = false)
+        &$output, $is_well_known = false, $depth = 0)
     {
         switch ($field->getType()) {
             case GPBType::SFIXED32:
@@ -218,7 +224,7 @@
             //      trigger_error("Not implemented.", E_ERROR);
             //      break;
             case GPBType::MESSAGE:
-                $value->serializeToJsonStream($output);
+                $value->serializeToJsonStream($output, $depth);
                 break;
             default:
                 user_error("Unsupported type.");
diff --git a/php/src/Google/Protobuf/Internal/Message.php b/php/src/Google/Protobuf/Internal/Message.php
index fd5928f..8c3c0d8 100644
--- a/php/src/Google/Protobuf/Internal/Message.php
+++ b/php/src/Google/Protobuf/Internal/Message.php
@@ -807,7 +807,8 @@
         $value,
         $field,
         $ignore_unknown,
-        $is_map_key = false)
+        $is_map_key = false,
+        $depth = 0)
     {
         switch ($field->getType()) {
             case GPBType::MESSAGE:
@@ -855,7 +856,7 @@
                     } elseif (!is_object($value) && !is_array($value)) {
                         throw new GPBDecodeException("Expect message.");
                     }
-                    $submsg->mergeFromJsonArray($value, $ignore_unknown);
+                    $submsg->mergeFromJsonArray($value, $ignore_unknown, $depth);
                 }
                 return $submsg;
             case GPBType::ENUM:
@@ -1135,17 +1136,20 @@
         }
     }
 
-    protected function mergeFromJsonArray($array, $ignore_unknown)
+    protected function mergeFromJsonArray($array, $ignore_unknown, $depth = 0)
     {
         if (is_a($this, "Google\Protobuf\Any")) {
+            if ($depth >= 100) {
+                throw new GPBDecodeException("Max nesting exceeded");
+            }
             $this->clear();
             $this->setTypeUrl($array["@type"]);
             $msg = $this->unpack();
             if (GPBUtil::hasSpecialJsonMapping($msg)) {
-                $msg->mergeFromJsonArray($array["value"], $ignore_unknown);
+                $msg->mergeFromJsonArray($array["value"], $ignore_unknown, $depth + 1);
             } else {
                 unset($array["@type"]);
-                $msg->mergeFromJsonArray($array, $ignore_unknown);
+                $msg->mergeFromJsonArray($array, $ignore_unknown, $depth);
             }
             $this->setValue($msg->serializeToString());
             return;
@@ -1181,7 +1185,7 @@
             $fields = $this->getFields();
             foreach($array as $key => $value) {
                 $v = new Value();
-                $v->mergeFromJsonArray($value, $ignore_unknown);
+                $v->mergeFromJsonArray($value, $ignore_unknown, $depth);
                 $fields[$key] = $v;
             }
             return;
@@ -1205,7 +1209,7 @@
                     }
                     foreach ($array as $key => $v) {
                         $value = new Value();
-                        $value->mergeFromJsonArray($v, $ignore_unknown);
+                        $value->mergeFromJsonArray($v, $ignore_unknown, $depth);
                         $values = $struct_value->getFields();
                         $values[$key]= $value;
                     }
@@ -1218,7 +1222,7 @@
                     }
                     foreach ($array as $v) {
                         $value = new Value();
-                        $value->mergeFromJsonArray($v, $ignore_unknown);
+                        $value->mergeFromJsonArray($v, $ignore_unknown, $depth);
                         $values = $list_value->getValues();
                         $values[]= $value;
                     }
@@ -1228,10 +1232,10 @@
             }
             return;
         }
-        $this->mergeFromArrayJsonImpl($array, $ignore_unknown);
+        $this->mergeFromArrayJsonImpl($array, $ignore_unknown, $depth);
     }
 
-    private function mergeFromArrayJsonImpl($array, $ignore_unknown)
+    private function mergeFromArrayJsonImpl($array, $ignore_unknown, $depth = 0)
     {
         foreach ($array as $key => $value) {
             $field = $this->desc->getFieldByJsonName($key);
@@ -1262,11 +1266,14 @@
                         $tmp_key,
                         $key_field,
                         $ignore_unknown,
-                        true);
+                        true,
+                        $depth);
                     $proto_value = $this->convertJsonValueToProtoValue(
                         $tmp_value,
                         $value_field,
-                        $ignore_unknown);
+                        $ignore_unknown,
+                        false,
+                        $depth);
 
                     // Mapped unknown enum string values should be silently
                     // ignored if ignore_unknown is set.
@@ -1293,7 +1300,9 @@
                     $proto_value = $this->convertJsonValueToProtoValue(
                         $tmp,
                         $field,
-                        $ignore_unknown);
+                        $ignore_unknown,
+                        false,
+                        $depth);
 
                     // Repeated unknown enum string values should be silently
                     // ignored if ignore_unknown is set.
@@ -1311,7 +1320,9 @@
                 $proto_value = $this->convertJsonValueToProtoValue(
                     $value,
                     $field,
-                    $ignore_unknown);
+                    $ignore_unknown,
+                    false,
+                    $depth);
                 if ($field->getType() === GPBType::MESSAGE) {
                     if (is_null($proto_value)) {
                         continue;
@@ -1453,12 +1464,12 @@
     /**
      * @ignore
      */
-    private function serializeFieldToJsonStream(&$output, $field)
+    private function serializeFieldToJsonStream(&$output, $field, $depth = 0)
     {
         $getter = $field->getGetter();
         $values = $this->$getter();
         return GPBJsonWire::serializeFieldToStream(
-            $values, $field, $output, !GPBUtil::hasSpecialJsonMapping($this));
+            $values, $field, $output, !GPBUtil::hasSpecialJsonMapping($this), $depth);
     }
 
     /**
@@ -1479,10 +1490,13 @@
     /**
      * @ignore
      */
-    public function serializeToJsonStream(&$output)
+    public function serializeToJsonStream(&$output, $depth = 0)
     {
         $options = $output->getOptions();
         if (is_a($this, 'Google\Protobuf\Any')) {
+            if ($depth >= 100) {
+                throw new \Exception("Max nesting exceeded");
+            }
             $output->writeRaw("{", 1);
             $type_field = $this->desc->getFieldByNumber(1);
             $value_msg = $this->unpack();
@@ -1496,13 +1510,13 @@
             // Serialize value
             if (GPBUtil::hasSpecialJsonMapping($value_msg)) {
                 $output->writeRaw(",\"value\":", 9);
-                $value_msg->serializeToJsonStream($output);
+                $value_msg->serializeToJsonStream($output, $depth + 1);
             } else {
                 $value_fields = $value_msg->desc->getField();
                 foreach ($value_fields as $field) {
                     if ($value_msg->existField($field, $options)) {
                         $output->writeRaw(",", 1);
-                        if (!$value_msg->serializeFieldToJsonStream($output, $field)) {
+                        if (!$value_msg->serializeFieldToJsonStream($output, $field, $depth)) {
                             return false;
                         }
                     }
@@ -1529,7 +1543,7 @@
             if (!$this->existField($field, $options)) {
                 $output->writeRaw("[]", 2);
             } else {
-                if (!$this->serializeFieldToJsonStream($output, $field)) {
+                if (!$this->serializeFieldToJsonStream($output, $field, $depth)) {
                     return false;
                 }
             }
@@ -1538,7 +1552,7 @@
             if (!$this->existField($field, $options)) {
                 $output->writeRaw("{}", 2);
             } else {
-                if (!$this->serializeFieldToJsonStream($output, $field)) {
+                if (!$this->serializeFieldToJsonStream($output, $field, $depth)) {
                     return false;
                 }
             }
@@ -1556,7 +1570,7 @@
                     } else {
                         $output->writeRaw(",", 1);
                     }
-                    if (!$this->serializeFieldToJsonStream($output, $field)) {
+                    if (!$this->serializeFieldToJsonStream($output, $field, $depth)) {
                         return false;
                     }
                 }
@@ -1713,7 +1727,7 @@
     /**
      * @ignore
      */
-    private function fieldDataOnlyJsonByteSize($field, $value, $options = 0)
+    private function fieldDataOnlyJsonByteSize($field, $value, $options = 0, $depth = 0)
     {
         $size = 0;
 
@@ -1808,7 +1822,7 @@
                 $size += 2;  // size for \"\"
                 break;
             case GPBType::MESSAGE:
-                $size += $value->jsonByteSize($options);
+                $size += $value->jsonByteSize($options, $depth);
                 break;
 #             case GPBType::GROUP:
 #                 // TODO: Add support.
@@ -1886,7 +1900,10 @@
     /**
      * @ignore
      */
-    private function fieldJsonByteSize($field, $options = 0)
+    /**
+     * @ignore
+     */
+    private function fieldJsonByteSize($field, $options = 0, $depth = 0)
     {
         $size = 0;
 
@@ -1926,8 +1943,8 @@
                         if ($additional_quote) {
                             $size += 2;  // size for ""
                         }
-                        $size += $this->fieldDataOnlyJsonByteSize($key_field, $key, $options);
-                        $size += $this->fieldDataOnlyJsonByteSize($value_field, $value, $options);
+                        $size += $this->fieldDataOnlyJsonByteSize($key_field, $key, $options, $depth);
+                        $size += $this->fieldDataOnlyJsonByteSize($value_field, $value, $options, $depth);
                         $size += 1;  // size for :
                     }
                 }
@@ -1950,7 +1967,7 @@
                     $size += $count - 1;                     // size for commas
                     $getter = $field->getGetter();
                     foreach ($values as $value) {
-                        $size += $this->fieldDataOnlyJsonByteSize($field, $value, $options);
+                        $size += $this->fieldDataOnlyJsonByteSize($field, $value, $options, $depth);
                     }
                 }
             }
@@ -1965,7 +1982,7 @@
             }
             $getter = $field->getGetter();
             $value = $this->$getter();
-            $size += $this->fieldDataOnlyJsonByteSize($field, $value, $options);
+            $size += $this->fieldDataOnlyJsonByteSize($field, $value, $options, $depth);
         }
         return $size;
     }
@@ -2014,10 +2031,13 @@
     /**
      * @ignore
      */
-    public function jsonByteSize($options = 0)
+    public function jsonByteSize($options = 0, $depth = 0)
     {
         $size = 0;
         if (is_a($this, 'Google\Protobuf\Any')) {
+            if ($depth >= 100) {
+                throw new \Exception("Max nesting exceeded");
+            }
             // Size for "{}".
             $size += 2;
 
@@ -2031,9 +2051,9 @@
             if (GPBUtil::hasSpecialJsonMapping($value_msg)) {
                 // Size for "\",value\":".
                 $size += 9;
-                $size += $value_msg->jsonByteSize($options);
+                $size += $value_msg->jsonByteSize($options, $depth + 1);
             } else {
-                $value_size = $value_msg->jsonByteSize($options);
+                $value_size = $value_msg->jsonByteSize($options, $depth);
                 // size === 2 it's empty message {} which is not serialized inside any
                 if ($value_size !== 2) {
                     // Size for value. +1 for comma, -2 for "{}".
@@ -2053,7 +2073,7 @@
         } elseif (get_class($this) === 'Google\Protobuf\ListValue') {
             $field = $this->desc->getField()[1];
             if ($this->existField($field, $options)) {
-                $field_size = $this->fieldJsonByteSize($field, $options);
+                $field_size = $this->fieldJsonByteSize($field, $options, $depth);
                 $size += $field_size;
             } else {
                 // Size for "[]".
@@ -2062,7 +2082,7 @@
         } elseif (get_class($this) === 'Google\Protobuf\Struct') {
             $field = $this->desc->getField()[1];
             if ($this->existField($field, $options)) {
-                $field_size = $this->fieldJsonByteSize($field, $options);
+                $field_size = $this->fieldJsonByteSize($field, $options, $depth);
                 $size += $field_size;
             } else {
                 // Size for "{}".
@@ -2077,7 +2097,7 @@
             $fields = $this->desc->getField();
             $count = 0;
             foreach ($fields as $field) {
-                $field_size = $this->fieldJsonByteSize($field, $options);
+                $field_size = $this->fieldJsonByteSize($field, $options, $depth);
                 $size += $field_size;
                 if ($field_size != 0) {
                   $count++;
diff --git a/php/tests/EncodeDecodeTest.php b/php/tests/EncodeDecodeTest.php
index 65ac654..c88f19f 100644
--- a/php/tests/EncodeDecodeTest.php
+++ b/php/tests/EncodeDecodeTest.php
@@ -2141,6 +2141,59 @@
         $this->assertTrue($threwTooLarge, 'recursion_limit > 65535 must throw');
     }
 
+    private function makeRecursiveAny($depth)
+    {
+        $leaf = new Int32Value();
+        $leaf->setValue(123);
+        $cur = new Any();
+        $cur->pack($leaf);
+        for ($i = 0; $i < $depth; $i++) {
+            $parent = new Any();
+            $parent->pack($cur);
+            $cur = $parent;
+        }
+        return $cur;
+    }
+
+    public function testAnyJsonRecursionLimit()
+    {
+        // 99 nested Any wrapping a leaf Int32Value is 100 Any levels total.
+        $msg = $this->makeRecursiveAny(99);
+        $json = $msg->serializeToJsonString();
+        $this->assertNotEquals('', $json);
+
+        // 100 nested Any wrapping a leaf Int32Value is 101 Any levels total, exceeding the limit of 100.
+        $deep = $this->makeRecursiveAny(100);
+        try {
+            $deep->serializeToJsonString();
+            $this->fail('Expected an exception for exceeding Any recursion depth limit');
+        } catch (Exception $e) {
+            $this->assertStringContainsString('Max nesting exceeded', $e->getMessage());
+        }
+    }
+
+    public function testAnyJsonDecodeRecursionLimit()
+    {
+        // Build JSON string with 99 nested Any wrapping a leaf Int32Value.
+        $json99 = '{"@type":"type.googleapis.com/google.protobuf.Int32Value","value":123}';
+        for ($i = 0; $i < 99; $i++) {
+            $json99 = '{"@type":"type.googleapis.com/google.protobuf.Any","value":' . $json99 . '}';
+        }
+        $any = new Any();
+        $any->mergeFromJsonString($json99);
+        $this->assertSame("type.googleapis.com/google.protobuf.Any", $any->getTypeUrl());
+
+        // Build JSON string with 100 nested Any (101 levels total), exceeding limit of 100.
+        $json100 = '{"@type":"type.googleapis.com/google.protobuf.Any","value":' . $json99 . '}';
+        try {
+            $any = new Any();
+            $any->mergeFromJsonString($json100);
+            $this->fail('Expected an exception for exceeding Any recursion depth limit in JSON decode');
+        } catch (Exception $e) {
+            $this->assertStringContainsString('Max nesting exceeded', $e->getMessage());
+        }
+    }
+
     public function testLargeMessageWithSubMessage()
     {
         // 33MB string to exceed the 32MB former limit.
diff --git a/php/tests/WellKnownTest.php b/php/tests/WellKnownTest.php
index 790f52f..ea5f3ac 100644
--- a/php/tests/WellKnownTest.php
+++ b/php/tests/WellKnownTest.php
@@ -114,6 +114,33 @@
         $any->unpack();
     }
 
+    public function testAnyJsonRecursionLimit()
+    {
+        $leaf = new Int32Value();
+        $leaf->setValue(123);
+        $cur = new Any();
+        $cur->pack($leaf);
+        for ($i = 0; $i < 99; $i++) {
+            $parent = new Any();
+            $parent->pack($cur);
+            $cur = $parent;
+        }
+
+        // 100 Any levels total: serialization succeeds.
+        $json = $cur->serializeToJsonString();
+        $this->assertNotEquals('', $json);
+
+        // 101 Any levels total: serialization exceeds limit of 100 and throws.
+        $parent = new Any();
+        $parent->pack($cur);
+        try {
+            $parent->serializeToJsonString();
+            $this->fail('Expected an exception for exceeding Any recursion depth limit');
+        } catch (Exception $e) {
+            $this->assertStringContainsString('Max nesting exceeded', $e->getMessage());
+        }
+    }
+
     public function testApi()
     {
         $m = new Api();
diff --git a/upb/json/encode.c b/upb/json/encode.c
index 81fdc33..b1964c4 100644
--- a/upb/json/encode.c
+++ b/upb/json/encode.c
@@ -36,6 +36,7 @@
   char *buf, *ptr, *end;
   size_t overflow;
   int indent_depth;
+  int depth;
   int options;
   const upb_DefPool* ext_pool;
   jmp_buf err;
@@ -385,6 +386,9 @@
 
 static void jsonenc_any(jsonenc* e, const upb_Message* msg,
                         const upb_MessageDef* m) {
+  if (++e->depth > 100) {
+    jsonenc_err(e, "Max nesting exceeded");
+  }
   const upb_FieldDef* type_url_f = upb_MessageDef_FindFieldByNumber(m, 1);
   const upb_FieldDef* value_f = upb_MessageDef_FindFieldByNumber(m, 2);
   upb_StringView type_url = upb_Message_GetFieldByDef(msg, type_url_f).str_val;
@@ -416,6 +420,7 @@
   }
 
   jsonenc_putstr(e, "}");
+  --e->depth;
 }
 
 static void jsonenc_putsep(jsonenc* e, const char* str, bool* first) {
@@ -800,6 +805,8 @@
   e.ptr = buf;
   e.end = UPB_PTRADD(buf, size);
   e.overflow = 0;
+  e.indent_depth = 0;
+  e.depth = 0;
   e.options = options;
   e.ext_pool = ext_pool;
   e.status = status;