Skip to content

Commit 08674aa

Browse files
committed
Added maximum nesting level; #34
1 parent 3ac690f commit 08674aa

File tree

2 files changed

+186
-1
lines changed

2 files changed

+186
-1
lines changed

ph-json/src/main/java/com/helger/json/parser/JsonParser.java

+54-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import javax.annotation.WillNotClose;
2626
import javax.annotation.concurrent.NotThreadSafe;
2727

28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
2831
import com.helger.commons.ValueEnforcer;
2932
import com.helger.commons.io.stream.NonBlockingPushbackReader;
3033
import com.helger.commons.state.EEOI;
@@ -47,7 +50,7 @@ private enum EStringQuoteMode
4750
DOUBLE ('"'),
4851
SINGLE ('\'');
4952

50-
private char m_cQuote;
53+
private final char m_cQuote;
5154

5255
EStringQuoteMode (final char cQuote)
5356
{
@@ -77,7 +80,9 @@ public static EStringQuoteMode getFromCharOrDefault (final int c)
7780
public static final boolean DEFAULT_REQUIRE_STRING_QUOTES = true;
7881
public static final boolean DEFAULT_ALLOW_SPECIAL_CHARS_IN_STRING = false;
7982
public static final boolean DEFAULT_CHECK_FOR_EOI = true;
83+
public static final int DEFAULT_MAX_NESTING_DEPTH = 1000;
8084

85+
private static final Logger LOGGER = LoggerFactory.getLogger (JsonParser.class);
8186
private static final int MAX_PUSH_BACK_CHARS = 2;
8287

8388
// Constructor parameters
@@ -91,11 +96,13 @@ public static EStringQuoteMode getFromCharOrDefault (final int c)
9196
private boolean m_bRequireStringQuotes = DEFAULT_REQUIRE_STRING_QUOTES;
9297
private boolean m_bAllowSpecialCharsInStrings = DEFAULT_ALLOW_SPECIAL_CHARS_IN_STRING;
9398
private boolean m_bCheckForEOI = DEFAULT_CHECK_FOR_EOI;
99+
private int m_nMaxNestingDepth = DEFAULT_MAX_NESTING_DEPTH;
94100

95101
// Status variables
96102
// Position tracking
97103
private final JsonParsePosition m_aParsePos = new JsonParsePosition ();
98104
private int m_nBackupChars = 0;
105+
private int m_nNestingLevel = 0;
99106
// string reading cache
100107
private final JsonStringBuilder m_aSB1 = new JsonStringBuilder (256);
101108
private final JsonStringBuilder m_aSB2 = new JsonStringBuilder (256);
@@ -209,6 +216,30 @@ public JsonParser setCheckForEOI (final boolean bCheckForEOI)
209216
return this;
210217
}
211218

219+
/**
220+
* @return The maximum nesting depth of the JSON to read. Always > 0.
221+
* @since 11.0.5
222+
*/
223+
@Nonnegative
224+
public final int getMaxNestingDepth ()
225+
{
226+
return m_nMaxNestingDepth;
227+
}
228+
229+
/**
230+
* @param nMaxNestingDepth
231+
* The maximum nesting depth of the JSON to read. Must be > 0.
232+
* @return this for chaining
233+
* @since 11.0.5
234+
*/
235+
@Nonnull
236+
public final JsonParser setMaxNestingDepth (@Nonnegative final int nMaxNestingDepth)
237+
{
238+
ValueEnforcer.isGT0 (nMaxNestingDepth, "MaxNestingDepth");
239+
m_nMaxNestingDepth = nMaxNestingDepth;
240+
return this;
241+
}
242+
212243
/**
213244
* @return The current line number. First line has a value of 1.
214245
*/
@@ -894,6 +925,24 @@ private void _readObject () throws JsonParseException
894925
m_aCallback.onObjectEnd ();
895926
}
896927

928+
private void _incNestingLevel (@Nullable final IJsonParsePosition aTokenStart) throws JsonParseException
929+
{
930+
m_nNestingLevel++;
931+
if (m_nNestingLevel > m_nMaxNestingDepth)
932+
throw _parseEx (aTokenStart,
933+
"The nesting level " +
934+
m_nNestingLevel +
935+
" exceeds the maximum nesting level of " +
936+
m_nMaxNestingDepth);
937+
}
938+
939+
private void _decNestingLevel ()
940+
{
941+
m_nNestingLevel--;
942+
if (m_nNestingLevel < 0)
943+
LOGGER.warn ("Internal inconsistency: nesting level < 0: " + m_nNestingLevel);
944+
}
945+
897946
/**
898947
* Read a single value
899948
*
@@ -956,10 +1005,14 @@ private EEOI _readValue () throws JsonParseException
9561005
m_aCallback.onNull ();
9571006
break;
9581007
case CJson.ARRAY_START:
1008+
_incNestingLevel (aStartPos);
9591009
_readArray ();
1010+
_decNestingLevel ();
9601011
break;
9611012
case CJson.OBJECT_START:
1013+
_incNestingLevel (aStartPos);
9621014
_readObject ();
1015+
_decNestingLevel ();
9631016
break;
9641017
case EOI:
9651018
return EEOI.EOI;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.helger.json.supplementary.issues;
2+
3+
import static org.junit.Assert.assertNotNull;
4+
import static org.junit.Assert.assertNull;
5+
import static org.junit.Assert.assertTrue;
6+
7+
import javax.annotation.Nonnegative;
8+
import javax.annotation.Nonnull;
9+
10+
import org.junit.Test;
11+
12+
import com.helger.commons.wrapper.Wrapper;
13+
import com.helger.json.IJson;
14+
import com.helger.json.IJsonArray;
15+
import com.helger.json.IJsonObject;
16+
import com.helger.json.parser.JsonParseException;
17+
import com.helger.json.parser.JsonParser;
18+
import com.helger.json.serialize.JsonReader;
19+
20+
public final class TestIssue34
21+
{
22+
@Nonnull
23+
public static String _createNestedDoc (@Nonnegative final int nNesting,
24+
@Nonnull final String sBeginning,
25+
@Nonnull final String sLevelOpen,
26+
@Nonnull final String sContent,
27+
@Nonnull final String sLevelClose,
28+
@Nonnull final String sEnd)
29+
{
30+
final StringBuilder ret = new StringBuilder (sBeginning.length () +
31+
nNesting * (sLevelOpen.length () + sLevelClose.length ()) +
32+
1 +
33+
sContent.length () +
34+
1 +
35+
(nNesting / 32) * 2 +
36+
sEnd.length ());
37+
ret.append (sBeginning);
38+
for (int i = 0; i < nNesting; ++i)
39+
{
40+
ret.append (sLevelOpen);
41+
if ((i & 31) == 0)
42+
ret.append ('\n');
43+
}
44+
ret.append ('\n').append (sContent).append ('\n');
45+
for (int i = 0; i < nNesting; ++i)
46+
{
47+
ret.append (sLevelClose);
48+
if ((i & 31) == 0)
49+
ret.append ('\n');
50+
}
51+
ret.append (sEnd);
52+
return ret.toString ();
53+
}
54+
55+
@Test
56+
public void testArrayMax ()
57+
{
58+
// Stack overflow with JSON array with nesting 5176
59+
final int nMax = JsonParser.DEFAULT_MAX_NESTING_DEPTH * 2;
60+
for (int nNesting = 1; nNesting < nMax; ++nNesting)
61+
{
62+
final String sNestedDoc = _createNestedDoc (nNesting, "", "[", "0", "]", "");
63+
final Wrapper <JsonParseException> aWrapper = new Wrapper <> ();
64+
final IJson aJson = JsonReader.builder ().source (sNestedDoc).customExceptionCallback (aWrapper::set).read ();
65+
if (aWrapper.isSet ())
66+
{
67+
assertNull (aJson);
68+
assertTrue ("Failed nesting is " + nNesting, nNesting > JsonParser.DEFAULT_MAX_NESTING_DEPTH);
69+
}
70+
else
71+
{
72+
assertNotNull (aJson);
73+
assertTrue (nNesting <= JsonParser.DEFAULT_MAX_NESTING_DEPTH);
74+
}
75+
}
76+
}
77+
78+
@Test
79+
public void testArrayMin ()
80+
{
81+
// Default: nesting okay
82+
IJsonArray aJson = JsonReader.builder ().source ("[[0]]").readAsArray ();
83+
assertNotNull (aJson);
84+
// Nested too deep
85+
aJson = JsonReader.builder ().source ("[[0]]").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsArray ();
86+
assertNull (aJson);
87+
// Nesting okay
88+
aJson = JsonReader.builder ().source ("[0]").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsArray ();
89+
assertNotNull (aJson);
90+
}
91+
92+
@Test
93+
public void testObjectMax ()
94+
{
95+
// Stack overflow with JSON object with nesting 5650
96+
// Start with 2, because we always have an outer bracket
97+
final int nMax = JsonParser.DEFAULT_MAX_NESTING_DEPTH * 2;
98+
for (int nNesting = 2; nNesting < nMax; ++nNesting)
99+
{
100+
final String sNestedDoc = _createNestedDoc (nNesting - 1, "{", "'a':{ ", "'b':0", "} ", "}");
101+
final Wrapper <JsonParseException> aWrapper = new Wrapper <> ();
102+
final IJson aJson = JsonReader.builder ().source (sNestedDoc).customExceptionCallback (aWrapper::set).read ();
103+
if (aWrapper.isSet ())
104+
{
105+
assertNull (aJson);
106+
assertTrue ("Failed nesting is " + nNesting, nNesting > JsonParser.DEFAULT_MAX_NESTING_DEPTH);
107+
}
108+
else
109+
{
110+
assertNotNull (aJson);
111+
assertTrue (nNesting <= JsonParser.DEFAULT_MAX_NESTING_DEPTH);
112+
}
113+
}
114+
}
115+
116+
@Test
117+
public void testObjectMin ()
118+
{
119+
// Default: nesting okay
120+
IJsonObject aJson = JsonReader.builder ().source ("{'a':{'a':0}}").readAsObject ();
121+
assertNotNull (aJson);
122+
// Nested too deep
123+
aJson = JsonReader.builder ()
124+
.source ("{'a':{'a':0}}")
125+
.customizeCallback (p -> p.setMaxNestingDepth (1))
126+
.readAsObject ();
127+
assertNull (aJson);
128+
// Nesting okay
129+
aJson = JsonReader.builder ().source ("{'a':0}").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsObject ();
130+
assertNotNull (aJson);
131+
}
132+
}

0 commit comments

Comments
 (0)