Skip to content

Commit

Permalink
Added maximum nesting level; #34
Browse files Browse the repository at this point in the history
  • Loading branch information
phax committed Jun 18, 2023
1 parent 3ac690f commit 08674aa
Show file tree
Hide file tree
Showing 2 changed files with 186 additions and 1 deletion.
55 changes: 54 additions & 1 deletion ph-json/src/main/java/com/helger/json/parser/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
import javax.annotation.WillNotClose;
import javax.annotation.concurrent.NotThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.io.stream.NonBlockingPushbackReader;
import com.helger.commons.state.EEOI;
Expand All @@ -47,7 +50,7 @@ private enum EStringQuoteMode
DOUBLE ('"'),
SINGLE ('\'');

private char m_cQuote;
private final char m_cQuote;

EStringQuoteMode (final char cQuote)
{
Expand Down Expand Up @@ -77,7 +80,9 @@ public static EStringQuoteMode getFromCharOrDefault (final int c)
public static final boolean DEFAULT_REQUIRE_STRING_QUOTES = true;
public static final boolean DEFAULT_ALLOW_SPECIAL_CHARS_IN_STRING = false;
public static final boolean DEFAULT_CHECK_FOR_EOI = true;
public static final int DEFAULT_MAX_NESTING_DEPTH = 1000;

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

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

// Status variables
// Position tracking
private final JsonParsePosition m_aParsePos = new JsonParsePosition ();
private int m_nBackupChars = 0;
private int m_nNestingLevel = 0;
// string reading cache
private final JsonStringBuilder m_aSB1 = new JsonStringBuilder (256);
private final JsonStringBuilder m_aSB2 = new JsonStringBuilder (256);
Expand Down Expand Up @@ -209,6 +216,30 @@ public JsonParser setCheckForEOI (final boolean bCheckForEOI)
return this;
}

/**
* @return The maximum nesting depth of the JSON to read. Always > 0.
* @since 11.0.5
*/
@Nonnegative
public final int getMaxNestingDepth ()
{
return m_nMaxNestingDepth;
}

/**
* @param nMaxNestingDepth
* The maximum nesting depth of the JSON to read. Must be > 0.
* @return this for chaining
* @since 11.0.5
*/
@Nonnull
public final JsonParser setMaxNestingDepth (@Nonnegative final int nMaxNestingDepth)
{
ValueEnforcer.isGT0 (nMaxNestingDepth, "MaxNestingDepth");
m_nMaxNestingDepth = nMaxNestingDepth;
return this;
}

/**
* @return The current line number. First line has a value of 1.
*/
Expand Down Expand Up @@ -894,6 +925,24 @@ private void _readObject () throws JsonParseException
m_aCallback.onObjectEnd ();
}

private void _incNestingLevel (@Nullable final IJsonParsePosition aTokenStart) throws JsonParseException
{
m_nNestingLevel++;
if (m_nNestingLevel > m_nMaxNestingDepth)
throw _parseEx (aTokenStart,
"The nesting level " +
m_nNestingLevel +
" exceeds the maximum nesting level of " +
m_nMaxNestingDepth);
}

private void _decNestingLevel ()
{
m_nNestingLevel--;
if (m_nNestingLevel < 0)
LOGGER.warn ("Internal inconsistency: nesting level < 0: " + m_nNestingLevel);
}

/**
* Read a single value
*
Expand Down Expand Up @@ -956,10 +1005,14 @@ private EEOI _readValue () throws JsonParseException
m_aCallback.onNull ();
break;
case CJson.ARRAY_START:
_incNestingLevel (aStartPos);
_readArray ();
_decNestingLevel ();
break;
case CJson.OBJECT_START:
_incNestingLevel (aStartPos);
_readObject ();
_decNestingLevel ();
break;
case EOI:
return EEOI.EOI;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.helger.json.supplementary.issues;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;

import org.junit.Test;

import com.helger.commons.wrapper.Wrapper;
import com.helger.json.IJson;
import com.helger.json.IJsonArray;
import com.helger.json.IJsonObject;
import com.helger.json.parser.JsonParseException;
import com.helger.json.parser.JsonParser;
import com.helger.json.serialize.JsonReader;

public final class TestIssue34
{
@Nonnull
public static String _createNestedDoc (@Nonnegative final int nNesting,
@Nonnull final String sBeginning,
@Nonnull final String sLevelOpen,
@Nonnull final String sContent,
@Nonnull final String sLevelClose,
@Nonnull final String sEnd)
{
final StringBuilder ret = new StringBuilder (sBeginning.length () +
nNesting * (sLevelOpen.length () + sLevelClose.length ()) +
1 +
sContent.length () +
1 +
(nNesting / 32) * 2 +
sEnd.length ());
ret.append (sBeginning);
for (int i = 0; i < nNesting; ++i)
{
ret.append (sLevelOpen);
if ((i & 31) == 0)
ret.append ('\n');
}
ret.append ('\n').append (sContent).append ('\n');
for (int i = 0; i < nNesting; ++i)
{
ret.append (sLevelClose);
if ((i & 31) == 0)
ret.append ('\n');
}
ret.append (sEnd);
return ret.toString ();
}

@Test
public void testArrayMax ()
{
// Stack overflow with JSON array with nesting 5176
final int nMax = JsonParser.DEFAULT_MAX_NESTING_DEPTH * 2;
for (int nNesting = 1; nNesting < nMax; ++nNesting)
{
final String sNestedDoc = _createNestedDoc (nNesting, "", "[", "0", "]", "");
final Wrapper <JsonParseException> aWrapper = new Wrapper <> ();
final IJson aJson = JsonReader.builder ().source (sNestedDoc).customExceptionCallback (aWrapper::set).read ();
if (aWrapper.isSet ())
{
assertNull (aJson);
assertTrue ("Failed nesting is " + nNesting, nNesting > JsonParser.DEFAULT_MAX_NESTING_DEPTH);
}
else
{
assertNotNull (aJson);
assertTrue (nNesting <= JsonParser.DEFAULT_MAX_NESTING_DEPTH);
}
}
}

@Test
public void testArrayMin ()
{
// Default: nesting okay
IJsonArray aJson = JsonReader.builder ().source ("[[0]]").readAsArray ();
assertNotNull (aJson);
// Nested too deep
aJson = JsonReader.builder ().source ("[[0]]").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsArray ();
assertNull (aJson);
// Nesting okay
aJson = JsonReader.builder ().source ("[0]").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsArray ();
assertNotNull (aJson);
}

@Test
public void testObjectMax ()
{
// Stack overflow with JSON object with nesting 5650
// Start with 2, because we always have an outer bracket
final int nMax = JsonParser.DEFAULT_MAX_NESTING_DEPTH * 2;
for (int nNesting = 2; nNesting < nMax; ++nNesting)
{
final String sNestedDoc = _createNestedDoc (nNesting - 1, "{", "'a':{ ", "'b':0", "} ", "}");
final Wrapper <JsonParseException> aWrapper = new Wrapper <> ();
final IJson aJson = JsonReader.builder ().source (sNestedDoc).customExceptionCallback (aWrapper::set).read ();
if (aWrapper.isSet ())
{
assertNull (aJson);
assertTrue ("Failed nesting is " + nNesting, nNesting > JsonParser.DEFAULT_MAX_NESTING_DEPTH);
}
else
{
assertNotNull (aJson);
assertTrue (nNesting <= JsonParser.DEFAULT_MAX_NESTING_DEPTH);
}
}
}

@Test
public void testObjectMin ()
{
// Default: nesting okay
IJsonObject aJson = JsonReader.builder ().source ("{'a':{'a':0}}").readAsObject ();
assertNotNull (aJson);
// Nested too deep
aJson = JsonReader.builder ()
.source ("{'a':{'a':0}}")
.customizeCallback (p -> p.setMaxNestingDepth (1))
.readAsObject ();
assertNull (aJson);
// Nesting okay
aJson = JsonReader.builder ().source ("{'a':0}").customizeCallback (p -> p.setMaxNestingDepth (1)).readAsObject ();
assertNotNull (aJson);
}
}

0 comments on commit 08674aa

Please sign in to comment.