Skip to content

Commit c3ddacb

Browse files
fix: apply recursion limits when parsing JSON well-known types with deep arrays
PiperOrigin-RevId: 817105608
1 parent 3e8a7c4 commit c3ddacb

File tree

3 files changed

+104
-45
lines changed

3 files changed

+104
-45
lines changed

csharp/src/Google.Protobuf.Test/JsonParserTest.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using ProtobufTestMessages.Proto3;
1616
using ProtobufUnittest;
1717
using System;
18+
using System.Linq;
1819
using UnitTest.Issues.TestProtos;
1920

2021
namespace Google.Protobuf
@@ -906,6 +907,41 @@ public void MaliciousRecursion()
906907
Assert.Throws<InvalidProtocolBufferException>(() => parser63.Parse<TestRecursiveMessage>(data64));
907908
}
908909

910+
[Test]
911+
public void MaliciousRecursionOfObjectsInValue()
912+
{
913+
int depth = 64;
914+
string json = string.Join("", Enumerable.Repeat("{\"a\":", depth)) +
915+
"{}" +
916+
string.Join("", Enumerable.Repeat("}", depth));
917+
918+
// Each object level requires a Value and a Struct, so we can effectively only
919+
// handle half as much depth as in a normal message.
920+
var sufficientLimitParser = new JsonParser(new JsonParser.Settings(depth * 2 + 1));
921+
sufficientLimitParser.Parse<Value>(json);
922+
923+
var insufficientLimitParser = new JsonParser(new JsonParser.Settings(depth * 2));
924+
Assert.Throws<InvalidProtocolBufferException>(() => insufficientLimitParser.Parse<Value>(json));
925+
}
926+
927+
[Test]
928+
public void MaliciousRecursionOfArraysInValue()
929+
{
930+
int depth = 64;
931+
string json = new string('[', depth) + new string(']', depth);
932+
933+
// Each array level requires a Value and a ListValue, so we can effectively only
934+
// handle half as much depth as in a normal message. The limits here are slightly different than
935+
// the limits for object recursion due to implementation details, but the inconsistency
936+
// is preferred over the complexity of making arrays and objects match precisely in
937+
// limit handling.
938+
var sufficientLimitParser = new JsonParser(new JsonParser.Settings(depth * 2 - 1));
939+
sufficientLimitParser.Parse<Value>(json);
940+
941+
var insufficientLimitParser = new JsonParser(new JsonParser.Settings(depth * 2 - 2));
942+
Assert.Throws<InvalidProtocolBufferException>(() => insufficientLimitParser.Parse<Value>(json));
943+
}
944+
909945
[Test]
910946
[TestCase("AQI")]
911947
[TestCase("_-==")]

csharp/src/Google.Protobuf/JsonParser.cs

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -124,71 +124,87 @@ internal void Merge(IMessage message, TextReader jsonReader)
124124
/// of tokens provided by the tokenizer. This token stream is assumed to be valid JSON, with the
125125
/// tokenizer performing that validation - but not every token stream is valid "protobuf JSON".
126126
/// </summary>
127+
/// <remarks>
128+
/// This method maintains and checks the recursion depth, so *must* be called for any nested parsing.
129+
/// </remarks>
127130
private void Merge(IMessage message, JsonTokenizer tokenizer)
128131
{
129-
if (tokenizer.ObjectDepth > settings.RecursionLimit)
132+
if (tokenizer.RecursionDepth > settings.RecursionLimit)
130133
{
131134
throw InvalidProtocolBufferException.JsonRecursionLimitExceeded();
132135
}
133-
if (message.Descriptor.IsWellKnownType)
134-
{
135-
if (WellKnownTypeHandlers.TryGetValue(message.Descriptor.FullName, out Action<JsonParser, IMessage, JsonTokenizer> handler))
136-
{
137-
handler(this, message, tokenizer);
138-
return;
139-
}
140-
// Well-known types with no special handling continue in the normal way.
141-
}
142-
var token = tokenizer.Next();
143-
if (token.Type != JsonToken.TokenType.StartObject)
144-
{
145-
throw new InvalidProtocolBufferException("Expected an object");
146-
}
147-
var descriptor = message.Descriptor;
148-
var jsonFieldMap = descriptor.Fields.ByJsonName();
149-
// All the oneof fields we've already accounted for - we can only see each of them once.
150-
// The set is created lazily to avoid the overhead of creating a set for every message
151-
// we parsed, when oneofs are relatively rare.
152-
HashSet<OneofDescriptor> seenOneofs = null;
153-
while (true)
136+
tokenizer.RecursionDepth++;
137+
138+
// try/finally used in order to decrement the recursion depth regardless of outcome.
139+
// If an exception is thrown, the recursion depth is irrelevant anyway - but as the method
140+
// has multiple return statements, this is the simplest way of ensuring the recursion depth
141+
// is always decremented. An alternative would be to use a local function.
142+
try
154143
{
155-
token = tokenizer.Next();
156-
if (token.Type == JsonToken.TokenType.EndObject)
144+
if (message.Descriptor.IsWellKnownType)
157145
{
158-
return;
146+
if (WellKnownTypeHandlers.TryGetValue(message.Descriptor.FullName, out Action<JsonParser, IMessage, JsonTokenizer> handler))
147+
{
148+
handler(this, message, tokenizer);
149+
return;
150+
}
151+
// Well-known types with no special handling continue in the normal way.
159152
}
160-
if (token.Type != JsonToken.TokenType.Name)
153+
var token = tokenizer.Next();
154+
if (token.Type != JsonToken.TokenType.StartObject)
161155
{
162-
throw new InvalidOperationException("Unexpected token type " + token.Type);
156+
throw new InvalidProtocolBufferException("Expected an object");
163157
}
164-
string name = token.StringValue;
165-
if (jsonFieldMap.TryGetValue(name, out FieldDescriptor field))
158+
var descriptor = message.Descriptor;
159+
var jsonFieldMap = descriptor.Fields.ByJsonName();
160+
// All the oneof fields we've already accounted for - we can only see each of them once.
161+
// The set is created lazily to avoid the overhead of creating a set for every message
162+
// we parsed, when oneofs are relatively rare.
163+
HashSet<OneofDescriptor> seenOneofs = null;
164+
while (true)
166165
{
167-
if (field.ContainingOneof != null)
166+
token = tokenizer.Next();
167+
if (token.Type == JsonToken.TokenType.EndObject)
168168
{
169-
if (seenOneofs == null)
170-
{
171-
seenOneofs = new HashSet<OneofDescriptor>();
172-
}
173-
if (!seenOneofs.Add(field.ContainingOneof))
174-
{
175-
throw new InvalidProtocolBufferException($"Multiple values specified for oneof {field.ContainingOneof.Name}");
176-
}
169+
return;
177170
}
178-
MergeField(message, field, tokenizer);
179-
}
180-
else
181-
{
182-
if (settings.IgnoreUnknownFields)
171+
if (token.Type != JsonToken.TokenType.Name)
172+
{
173+
throw new InvalidOperationException("Unexpected token type " + token.Type);
174+
}
175+
string name = token.StringValue;
176+
if (jsonFieldMap.TryGetValue(name, out FieldDescriptor field))
183177
{
184-
tokenizer.SkipValue();
178+
if (field.ContainingOneof != null)
179+
{
180+
if (seenOneofs == null)
181+
{
182+
seenOneofs = new HashSet<OneofDescriptor>();
183+
}
184+
if (!seenOneofs.Add(field.ContainingOneof))
185+
{
186+
throw new InvalidProtocolBufferException($"Multiple values specified for oneof {field.ContainingOneof.Name}");
187+
}
188+
}
189+
MergeField(message, field, tokenizer);
185190
}
186191
else
187192
{
188-
throw new InvalidProtocolBufferException("Unknown field: " + name);
193+
if (settings.IgnoreUnknownFields)
194+
{
195+
tokenizer.SkipValue();
196+
}
197+
else
198+
{
199+
throw new InvalidProtocolBufferException("Unknown field: " + name);
200+
}
189201
}
190202
}
191203
}
204+
finally
205+
{
206+
tokenizer.RecursionDepth--;
207+
}
192208
}
193209

194210
private void MergeField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)

csharp/src/Google.Protobuf/JsonTokenizer.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ internal static JsonTokenizer FromReplayedTokens(IList<JsonToken> tokens, JsonTo
5151
return new JsonReplayTokenizer(tokens, continuation);
5252
}
5353

54+
/// <summary>
55+
/// The depth of recursion within JsonParser. This is not directly computable within the tokenizer itself,
56+
/// as arrays contribute to the recursion depth for ListValue parsing, but not for "normal" message types.
57+
/// This is maintained (and checked) by <see cref="JsonParser.Merge(IMessage, JsonTokenizer)"/>.
58+
/// </summary>
59+
internal int RecursionDepth { get; set; }
60+
5461
/// <summary>
5562
/// Returns the depth of the stack, purely in objects (not collections).
5663
/// Informally, this is the number of remaining unclosed '{' characters we have.

0 commit comments

Comments
 (0)