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;